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('%\((?P<name>[^\)]+)\)s')
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 not (h in range(24) and m in range(60) and s 2192 in range(60)): 2193 raise ValueError('Hours or minutes or seconds are outside of allowed range') 2194 value = datetime.time(h, m, s) 2195 return (value, None) 2196 except AttributeError: 2197 pass 2198 except ValueError: 2199 pass 2200 return (ivalue, translate(self.error_message))
2201
2202 # A UTC class. 2203 -class UTC(datetime.tzinfo):
2204 """UTC""" 2205 ZERO = datetime.timedelta(0)
2206 - def utcoffset(self, dt):
2207 return UTC.ZERO
2208 - def tzname(self, dt):
2209 return "UTC"
2210 - def dst(self, dt):
2211 return UTC.ZERO
2212 utc = UTC()
2213 2214 -class IS_DATE(Validator):
2215 """ 2216 example:: 2217 2218 INPUT(_type='text', _name='name', requires=IS_DATE()) 2219 2220 date has to be in the ISO8960 format YYYY-MM-DD 2221 """ 2222
2223 - def __init__(self, format='%Y-%m-%d', 2224 error_message='enter date as %(format)s', 2225 timezone = None):
2226 """ 2227 timezome must be None or a pytz.timezone("America/Chicago") object 2228 """ 2229 self.format = translate(format) 2230 self.error_message = str(error_message) 2231 self.timezone = timezone 2232 self.extremes = {}
2233
2234 - def __call__(self, value):
2235 ovalue = value 2236 if isinstance(value, datetime.date): 2237 if self.timezone is not None: 2238 value = value - datetime.timedelta(seconds=self.timezone*3600) 2239 return (value, None) 2240 try: 2241 (y, m, d, hh, mm, ss, t0, t1, t2) = \ 2242 time.strptime(value, str(self.format)) 2243 value = datetime.date(y, m, d) 2244 if self.timezone is not None: 2245 value = self.timezone.localize(value).astimezone(utc) 2246 return (value, None) 2247 except: 2248 self.extremes.update(IS_DATETIME.nice(self.format)) 2249 return (ovalue, translate(self.error_message) % self.extremes)
2250
2251 - def formatter(self, value):
2252 if value is None: 2253 return None 2254 format = self.format 2255 year = value.year 2256 y = '%.4i' % year 2257 format = format.replace('%y', y[-2:]) 2258 format = format.replace('%Y', y) 2259 if year < 1900: 2260 year = 2000 2261 d = datetime.date(year, value.month, value.day) 2262 if self.timezone is not None: 2263 d = d.replace(tzinfo=utc).astimezone(self.timezone) 2264 return d.strftime(format)
2265
2266 2267 -class IS_DATETIME(Validator):
2268 """ 2269 example:: 2270 2271 INPUT(_type='text', _name='name', requires=IS_DATETIME()) 2272 2273 datetime has to be in the ISO8960 format YYYY-MM-DD hh:mm:ss 2274 """ 2275 2276 isodatetime = '%Y-%m-%d %H:%M:%S' 2277 2278 @staticmethod
2279 - def nice(format):
2280 code = (('%Y', '1963'), 2281 ('%y', '63'), 2282 ('%d', '28'), 2283 ('%m', '08'), 2284 ('%b', 'Aug'), 2285 ('%B', 'August'), 2286 ('%H', '14'), 2287 ('%I', '02'), 2288 ('%p', 'PM'), 2289 ('%M', '30'), 2290 ('%S', '59')) 2291 for (a, b) in code: 2292 format = format.replace(a, b) 2293 return dict(format=format)
2294
2295 - def __init__(self, format='%Y-%m-%d %H:%M:%S', 2296 error_message='enter date and time as %(format)s', 2297 timezone=None):
2298 """ 2299 timezome must be None or a pytz.timezone("America/Chicago") object 2300 """ 2301 self.format = translate(format) 2302 self.error_message = str(error_message) 2303 self.extremes = {} 2304 self.timezone = timezone
2305
2306 - def __call__(self, value):
2307 ovalue = value 2308 if isinstance(value, datetime.datetime): 2309 return (value, None) 2310 try: 2311 (y, m, d, hh, mm, ss, t0, t1, t2) = \ 2312 time.strptime(value, str(self.format)) 2313 value = datetime.datetime(y, m, d, hh, mm, ss) 2314 if self.timezone is not None: 2315 value = self.timezone.localize(value).astimezone(utc) 2316 return (value, None) 2317 except: 2318 self.extremes.update(IS_DATETIME.nice(self.format)) 2319 return (ovalue, translate(self.error_message) % self.extremes)
2320
2321 - def formatter(self, value):
2322 if value is None: 2323 return None 2324 format = self.format 2325 year = value.year 2326 y = '%.4i' % year 2327 format = format.replace('%y', y[-2:]) 2328 format = format.replace('%Y', y) 2329 if year < 1900: 2330 year = 2000 2331 d = datetime.datetime(year, value.month, value.day, 2332 value.hour, value.minute, value.second) 2333 if self.timezone is not None: 2334 d = d.replace(tzinfo=utc).astimezone(self.timezone) 2335 return d.strftime(format)
2336
2337 2338 -class IS_DATE_IN_RANGE(IS_DATE):
2339 """ 2340 example:: 2341 2342 >>> v = IS_DATE_IN_RANGE(minimum=datetime.date(2008,1,1), \ 2343 maximum=datetime.date(2009,12,31), \ 2344 format="%m/%d/%Y",error_message="oops") 2345 2346 >>> v('03/03/2008') 2347 (datetime.date(2008, 3, 3), None) 2348 2349 >>> v('03/03/2010') 2350 ('03/03/2010', 'oops') 2351 2352 >>> v(datetime.date(2008,3,3)) 2353 (datetime.date(2008, 3, 3), None) 2354 2355 >>> v(datetime.date(2010,3,3)) 2356 (datetime.date(2010, 3, 3), 'oops') 2357 2358 """
2359 - def __init__(self, 2360 minimum=None, 2361 maximum=None, 2362 format='%Y-%m-%d', 2363 error_message=None, 2364 timezone=None):
2365 self.minimum = minimum 2366 self.maximum = maximum 2367 if error_message is None: 2368 if minimum is None: 2369 error_message = "enter date on or before %(max)s" 2370 elif maximum is None: 2371 error_message = "enter date on or after %(min)s" 2372 else: 2373 error_message = "enter date in range %(min)s %(max)s" 2374 IS_DATE.__init__(self, 2375 format=format, 2376 error_message=error_message, 2377 timezone=timezone) 2378 self.extremes = dict(min=minimum, max=maximum)
2379
2380 - def __call__(self, value):
2381 ovalue = value 2382 (value, msg) = IS_DATE.__call__(self, value) 2383 if msg is not None: 2384 return (value, msg) 2385 if self.minimum and self.minimum > value: 2386 return (ovalue, translate(self.error_message) % self.extremes) 2387 if self.maximum and value > self.maximum: 2388 return (ovalue, translate(self.error_message) % self.extremes) 2389 return (value, None)
2390
2391 2392 -class IS_DATETIME_IN_RANGE(IS_DATETIME):
2393 """ 2394 example:: 2395 2396 >>> v = IS_DATETIME_IN_RANGE(\ 2397 minimum=datetime.datetime(2008,1,1,12,20), \ 2398 maximum=datetime.datetime(2009,12,31,12,20), \ 2399 format="%m/%d/%Y %H:%M",error_message="oops") 2400 >>> v('03/03/2008 12:40') 2401 (datetime.datetime(2008, 3, 3, 12, 40), None) 2402 2403 >>> v('03/03/2010 10:34') 2404 ('03/03/2010 10:34', 'oops') 2405 2406 >>> v(datetime.datetime(2008,3,3,0,0)) 2407 (datetime.datetime(2008, 3, 3, 0, 0), None) 2408 2409 >>> v(datetime.datetime(2010,3,3,0,0)) 2410 (datetime.datetime(2010, 3, 3, 0, 0), 'oops') 2411 """
2412 - def __init__(self, 2413 minimum=None, 2414 maximum=None, 2415 format='%Y-%m-%d %H:%M:%S', 2416 error_message=None, 2417 timezone=None):
2418 self.minimum = minimum 2419 self.maximum = maximum 2420 if error_message is None: 2421 if minimum is None: 2422 error_message = "enter date and time on or before %(max)s" 2423 elif maximum is None: 2424 error_message = "enter date and time on or after %(min)s" 2425 else: 2426 error_message = "enter date and time in range %(min)s %(max)s" 2427 IS_DATETIME.__init__(self, 2428 format=format, 2429 error_message=error_message, 2430 timezone=timezone) 2431 self.extremes = dict(min=minimum, max=maximum)
2432
2433 - def __call__(self, value):
2434 ovalue = value 2435 (value, msg) = IS_DATETIME.__call__(self, value) 2436 if msg is not None: 2437 return (value, msg) 2438 if self.minimum and self.minimum > value: 2439 return (ovalue, translate(self.error_message) % self.extremes) 2440 if self.maximum and value > self.maximum: 2441 return (ovalue, translate(self.error_message) % self.extremes) 2442 return (value, None)
2443
2444 2445 -class IS_LIST_OF(Validator):
2446
2447 - def __init__(self, other=None, minimum=0, maximum=100, 2448 error_message=None):
2449 self.other = other 2450 self.minimum = minimum 2451 self.maximum = maximum 2452 self.error_message = error_message or "enter between %(min)g and %(max)g values"
2453
2454 - def __call__(self, value):
2455 ivalue = value 2456 if not isinstance(value, list): 2457 ivalue = [ivalue] 2458 if not self.minimum is None and len(ivalue) < self.minimum: 2459 return (ivalue, translate(self.error_message) % dict(min=self.minimum, max=self.maximum)) 2460 if not self.maximum is None and len(ivalue) > self.maximum: 2461 return (ivalue, translate(self.error_message) % dict(min=self.minimum, max=self.maximum)) 2462 new_value = [] 2463 if self.other: 2464 for item in ivalue: 2465 if item.strip(): 2466 (v, e) = self.other(item) 2467 if e: 2468 return (ivalue, e) 2469 else: 2470 new_value.append(v) 2471 ivalue = new_value 2472 return (ivalue, None)
2473
2474 2475 -class IS_LOWER(Validator):
2476 """ 2477 convert to lower case 2478 2479 >>> IS_LOWER()('ABC') 2480 ('abc', None) 2481 >>> IS_LOWER()('Ñ') 2482 ('\\xc3\\xb1', None) 2483 """ 2484
2485 - def __call__(self, value):
2486 return (value.decode('utf8').lower().encode('utf8'), None)
2487
2488 2489 -class IS_UPPER(Validator):
2490 """ 2491 convert to upper case 2492 2493 >>> IS_UPPER()('abc') 2494 ('ABC', None) 2495 >>> IS_UPPER()('ñ') 2496 ('\\xc3\\x91', None) 2497 """ 2498
2499 - def __call__(self, value):
2500 return (value.decode('utf8').upper().encode('utf8'), None)
2501
2502 2503 -def urlify(s, maxlen=80, keep_underscores=False):
2504 """ 2505 Convert incoming string to a simplified ASCII subset. 2506 if (keep_underscores): underscores are retained in the string 2507 else: underscores are translated to hyphens (default) 2508 """ 2509 if isinstance(s, str): 2510 s = s.decode('utf-8') # to unicode 2511 s = s.lower() # to lowercase 2512 s = unicodedata.normalize('NFKD', s) # normalize eg è => e, ñ => n 2513 s = s.encode('ascii', 'ignore') # encode as ASCII 2514 s = re.sub('&\w+?;', '', s) # strip html entities 2515 if keep_underscores: 2516 s = re.sub('\s+', '-', s) # whitespace to hyphens 2517 s = re.sub('[^\w\-]', '', s) 2518 # strip all but alphanumeric/underscore/hyphen 2519 else: 2520 s = re.sub('[\s_]+', '-', s) # whitespace & underscores to hyphens 2521 s = re.sub('[^a-z0-9\-]', '', s) # strip all but alphanumeric/hyphen 2522 s = re.sub('[-_][-_]+', '-', s) # collapse strings of hyphens 2523 s = s.strip('-') # remove leading and trailing hyphens 2524 return s[:maxlen] # enforce maximum length
2525
2526 2527 -class IS_SLUG(Validator):
2528 """ 2529 convert arbitrary text string to a slug 2530 2531 >>> IS_SLUG()('abc123') 2532 ('abc123', None) 2533 >>> IS_SLUG()('ABC123') 2534 ('abc123', None) 2535 >>> IS_SLUG()('abc-123') 2536 ('abc-123', None) 2537 >>> IS_SLUG()('abc--123') 2538 ('abc-123', None) 2539 >>> IS_SLUG()('abc 123') 2540 ('abc-123', None) 2541 >>> IS_SLUG()('abc\t_123') 2542 ('abc-123', None) 2543 >>> IS_SLUG()('-abc-') 2544 ('abc', None) 2545 >>> IS_SLUG()('--a--b--_ -c--') 2546 ('a-b-c', None) 2547 >>> IS_SLUG()('abc&amp;123') 2548 ('abc123', None) 2549 >>> IS_SLUG()('abc&amp;123&amp;def') 2550 ('abc123def', None) 2551 >>> IS_SLUG()('ñ') 2552 ('n', None) 2553 >>> IS_SLUG(maxlen=4)('abc123') 2554 ('abc1', None) 2555 >>> IS_SLUG()('abc_123') 2556 ('abc-123', None) 2557 >>> IS_SLUG(keep_underscores=False)('abc_123') 2558 ('abc-123', None) 2559 >>> IS_SLUG(keep_underscores=True)('abc_123') 2560 ('abc_123', None) 2561 >>> IS_SLUG(check=False)('abc') 2562 ('abc', None) 2563 >>> IS_SLUG(check=True)('abc') 2564 ('abc', None) 2565 >>> IS_SLUG(check=False)('a bc') 2566 ('a-bc', None) 2567 >>> IS_SLUG(check=True)('a bc') 2568 ('a bc', 'must be slug') 2569 """ 2570 2571 @staticmethod
2572 - def urlify(value, maxlen=80, keep_underscores=False):
2573 return urlify(value, maxlen, keep_underscores)
2574
2575 - def __init__(self, maxlen=80, check=False, error_message='must be slug', keep_underscores=False):
2576 self.maxlen = maxlen 2577 self.check = check 2578 self.error_message = error_message 2579 self.keep_underscores = keep_underscores
2580
2581 - def __call__(self, value):
2582 if self.check and value != urlify(value, self.maxlen, self.keep_underscores): 2583 return (value, translate(self.error_message)) 2584 return (urlify(value, self.maxlen, self.keep_underscores), None)
2585
2586 2587 -class ANY_OF(Validator):
2588 """ 2589 test if any of the validators in a list return successfully 2590 2591 >>> ANY_OF([IS_EMAIL(),IS_ALPHANUMERIC()])('a@b.co') 2592 ('a@b.co', None) 2593 >>> ANY_OF([IS_EMAIL(),IS_ALPHANUMERIC()])('abco') 2594 ('abco', None) 2595 >>> ANY_OF([IS_EMAIL(),IS_ALPHANUMERIC()])('@ab.co') 2596 ('@ab.co', 'enter only letters, numbers, and underscore') 2597 >>> ANY_OF([IS_ALPHANUMERIC(),IS_EMAIL()])('@ab.co') 2598 ('@ab.co', 'enter a valid email address') 2599 """ 2600
2601 - def __init__(self, subs):
2602 self.subs = subs
2603
2604 - def __call__(self, value):
2605 for validator in self.subs: 2606 value, error = validator(value) 2607 if error == None: 2608 break 2609 return value, error
2610
2611 - def formatter(self, value):
2612 # Use the formatter of the first subvalidator 2613 # that validates the value and has a formatter 2614 for validator in self.subs: 2615 if hasattr(validator, 'formatter') and validator(value)[1] != None: 2616 return validator.formatter(value)
2617
2618 2619 -class IS_EMPTY_OR(Validator):
2620 """ 2621 dummy class for testing IS_EMPTY_OR 2622 2623 >>> IS_EMPTY_OR(IS_EMAIL())('abc@def.com') 2624 ('abc@def.com', None) 2625 >>> IS_EMPTY_OR(IS_EMAIL())(' ') 2626 (None, None) 2627 >>> IS_EMPTY_OR(IS_EMAIL(), null='abc')(' ') 2628 ('abc', None) 2629 >>> IS_EMPTY_OR(IS_EMAIL(), null='abc', empty_regex='def')('def') 2630 ('abc', None) 2631 >>> IS_EMPTY_OR(IS_EMAIL())('abc') 2632 ('abc', 'enter a valid email address') 2633 >>> IS_EMPTY_OR(IS_EMAIL())(' abc ') 2634 ('abc', 'enter a valid email address') 2635 """ 2636
2637 - def __init__(self, other, null=None, empty_regex=None):
2638 (self.other, self.null) = (other, null) 2639 if empty_regex is not None: 2640 self.empty_regex = re.compile(empty_regex) 2641 else: 2642 self.empty_regex = None 2643 if hasattr(other, 'multiple'): 2644 self.multiple = other.multiple 2645 if hasattr(other, 'options'): 2646 self.options = self._options
2647
2648 - def _options(self):
2649 options = self.other.options() 2650 if (not options or options[0][0] != '') and not self.multiple: 2651 options.insert(0, ('', '')) 2652 return options
2653
2654 - def set_self_id(self, id):
2655 if isinstance(self.other, (list, tuple)): 2656 for item in self.other: 2657 if hasattr(item, 'set_self_id'): 2658 item.set_self_id(id) 2659 else: 2660 if hasattr(self.other, 'set_self_id'): 2661 self.other.set_self_id(id)
2662
2663 - def __call__(self, value):
2664 value, empty = is_empty(value, empty_regex=self.empty_regex) 2665 if empty: 2666 return (self.null, None) 2667 if isinstance(self.other, (list, tuple)): 2668 error = None 2669 for item in self.other: 2670 value, error = item(value) 2671 if error: 2672 break 2673 return value, error 2674 else: 2675 return self.other(value)
2676
2677 - def formatter(self, value):
2678 if hasattr(self.other, 'formatter'): 2679 return self.other.formatter(value) 2680 return value
2681 2682 IS_NULL_OR = IS_EMPTY_OR # for backward compatibility
2683 2684 2685 -class CLEANUP(Validator):
2686 """ 2687 example:: 2688 2689 INPUT(_type='text', _name='name', requires=CLEANUP()) 2690 2691 removes special characters on validation 2692 """ 2693 REGEX_CLEANUP = re.compile('[^\x09\x0a\x0d\x20-\x7e]') 2694
2695 - def __init__(self, regex=None):
2696 self.regex = self.REGEX_CLEANUP if regex is None \ 2697 else re.compile(regex)
2698
2699 - def __call__(self, value):
2700 v = self.regex.sub('', str(value).strip()) 2701 return (v, None)
2702
2703 2704 -class LazyCrypt(object):
2705 """ 2706 Stores a lazy password hash 2707 """
2708 - def __init__(self, crypt, password):
2709 """ 2710 crypt is an instance of the CRYPT validator, 2711 password is the password as inserted by the user 2712 """ 2713 self.crypt = crypt 2714 self.password = password 2715 self.crypted = None
2716
2717 - def __str__(self):
2718 """ 2719 Encrypted self.password and caches it in self.crypted. 2720 If self.crypt.salt the output is in the format <algorithm>$<salt>$<hash> 2721 2722 Try get the digest_alg from the key (if it exists) 2723 else assume the default digest_alg. If not key at all, set key='' 2724 2725 If a salt is specified use it, if salt is True, set salt to uuid 2726 (this should all be backward compatible) 2727 2728 Options: 2729 key = 'uuid' 2730 key = 'md5:uuid' 2731 key = 'sha512:uuid' 2732 ... 2733 key = 'pbkdf2(1000,64,sha512):uuid' 1000 iterations and 64 chars length 2734 """ 2735 if self.crypted: 2736 return self.crypted 2737 if self.crypt.key: 2738 if ':' in self.crypt.key: 2739 digest_alg, key = self.crypt.key.split(':', 1) 2740 else: 2741 digest_alg, key = self.crypt.digest_alg, self.crypt.key 2742 else: 2743 digest_alg, key = self.crypt.digest_alg, '' 2744 if self.crypt.salt: 2745 if self.crypt.salt == True: 2746 salt = str(web2py_uuid()).replace('-', '')[-16:] 2747 else: 2748 salt = self.crypt.salt 2749 else: 2750 salt = '' 2751 hashed = simple_hash(self.password, key, salt, digest_alg) 2752 self.crypted = '%s$%s$%s' % (digest_alg, salt, hashed) 2753 return self.crypted
2754
2755 - def __eq__(self, stored_password):
2756 """ 2757 compares the current lazy crypted password with a stored password 2758 """ 2759 2760 # LazyCrypt objects comparison 2761 if isinstance(stored_password, self.__class__): 2762 return ((self is stored_password) or 2763 ((self.crypt.key == stored_password.crypt.key) and 2764 (self.password == stored_password.password))) 2765 2766 if self.crypt.key: 2767 if ':' in self.crypt.key: 2768 key = self.crypt.key.split(':')[1] 2769 else: 2770 key = self.crypt.key 2771 else: 2772 key = '' 2773 if stored_password is None: 2774 return False 2775 elif stored_password.count('$') == 2: 2776 (digest_alg, salt, hash) = stored_password.split('$') 2777 h = simple_hash(self.password, key, salt, digest_alg) 2778 temp_pass = '%s$%s$%s' % (digest_alg, salt, h) 2779 else: # no salting 2780 # guess digest_alg 2781 digest_alg = DIGEST_ALG_BY_SIZE.get(len(stored_password), None) 2782 if not digest_alg: 2783 return False 2784 else: 2785 temp_pass = simple_hash(self.password, key, '', digest_alg) 2786 return temp_pass == stored_password
2787
2788 2789 -class CRYPT(object):
2790 """ 2791 example:: 2792 2793 INPUT(_type='text', _name='name', requires=CRYPT()) 2794 2795 encodes the value on validation with a digest. 2796 2797 If no arguments are provided CRYPT uses the MD5 algorithm. 2798 If the key argument is provided the HMAC+MD5 algorithm is used. 2799 If the digest_alg is specified this is used to replace the 2800 MD5 with, for example, SHA512. The digest_alg can be 2801 the name of a hashlib algorithm as a string or the algorithm itself. 2802 2803 min_length is the minimal password length (default 4) - IS_STRONG for serious security 2804 error_message is the message if password is too short 2805 2806 Notice that an empty password is accepted but invalid. It will not allow login back. 2807 Stores junk as hashed password. 2808 2809 Specify an algorithm or by default we will use sha512. 2810 2811 Typical available algorithms: 2812 md5, sha1, sha224, sha256, sha384, sha512 2813 2814 If salt, it hashes a password with a salt. 2815 If salt is True, this method will automatically generate one. 2816 Either case it returns an encrypted password string in the following format: 2817 2818 <algorithm>$<salt>$<hash> 2819 2820 Important: hashed password is returned as a LazyCrypt object and computed only if needed. 2821 The LasyCrypt object also knows how to compare itself with an existing salted password 2822 2823 Supports standard algorithms 2824 2825 >>> for alg in ('md5','sha1','sha256','sha384','sha512'): 2826 ... print str(CRYPT(digest_alg=alg,salt=True)('test')[0]) 2827 md5$...$... 2828 sha1$...$... 2829 sha256$...$... 2830 sha384$...$... 2831 sha512$...$... 2832 2833 The syntax is always alg$salt$hash 2834 2835 Supports for pbkdf2 2836 2837 >>> alg = 'pbkdf2(1000,20,sha512)' 2838 >>> print str(CRYPT(digest_alg=alg,salt=True)('test')[0]) 2839 pbkdf2(1000,20,sha512)$...$... 2840 2841 An optional hmac_key can be specified and it is used as salt prefix 2842 2843 >>> a = str(CRYPT(digest_alg='md5',key='mykey',salt=True)('test')[0]) 2844 >>> print a 2845 md5$...$... 2846 2847 Even if the algorithm changes the hash can still be validated 2848 2849 >>> CRYPT(digest_alg='sha1',key='mykey',salt=True)('test')[0] == a 2850 True 2851 2852 If no salt is specified CRYPT can guess the algorithms from length: 2853 2854 >>> a = str(CRYPT(digest_alg='sha1',salt=False)('test')[0]) 2855 >>> a 2856 'sha1$$a94a8fe5ccb19ba61c4c0873d391e987982fbbd3' 2857 >>> CRYPT(digest_alg='sha1',salt=False)('test')[0] == a 2858 True 2859 >>> CRYPT(digest_alg='sha1',salt=False)('test')[0] == a[6:] 2860 True 2861 >>> CRYPT(digest_alg='md5',salt=False)('test')[0] == a 2862 True 2863 >>> CRYPT(digest_alg='md5',salt=False)('test')[0] == a[6:] 2864 True 2865 """ 2866
2867 - def __init__(self, 2868 key=None, 2869 digest_alg='pbkdf2(1000,20,sha512)', 2870 min_length=0, 2871 error_message='too short', salt=True, 2872 max_length=1024):
2873 """ 2874 important, digest_alg='md5' is not the default hashing algorithm for 2875 web2py. This is only an example of usage of this function. 2876 2877 The actual hash algorithm is determined from the key which is 2878 generated by web2py in tools.py. This defaults to hmac+sha512. 2879 """ 2880 self.key = key 2881 self.digest_alg = digest_alg 2882 self.min_length = min_length 2883 self.max_length = max_length 2884 self.error_message = error_message 2885 self.salt = salt
2886
2887 - def __call__(self, value):
2888 value = value and value[:self.max_length] 2889 if len(value) < self.min_length: 2890 return ('', translate(self.error_message)) 2891 return (LazyCrypt(self, value), None)
2892 2893 # entropy calculator for IS_STRONG 2894 # 2895 lowerset = frozenset(unicode('abcdefghijklmnopqrstuvwxyz')) 2896 upperset = frozenset(unicode('ABCDEFGHIJKLMNOPQRSTUVWXYZ')) 2897 numberset = frozenset(unicode('0123456789')) 2898 sym1set = frozenset(unicode('!@#$%^&*()')) 2899 sym2set = frozenset(unicode('~`-_=+[]{}\\|;:\'",.<>?/')) 2900 otherset = frozenset( 2901 unicode('0123456789abcdefghijklmnopqrstuvwxyz')) # anything else
2902 2903 2904 -def calc_entropy(string):
2905 " calculate a simple entropy for a given string " 2906 import math 2907 alphabet = 0 # alphabet size 2908 other = set() 2909 seen = set() 2910 lastset = None 2911 if isinstance(string, str): 2912 string = unicode(string, encoding='utf8') 2913 for c in string: 2914 # classify this character 2915 inset = otherset 2916 for cset in (lowerset, upperset, numberset, sym1set, sym2set): 2917 if c in cset: 2918 inset = cset 2919 break 2920 # calculate effect of character on alphabet size 2921 if inset not in seen: 2922 seen.add(inset) 2923 alphabet += len(inset) # credit for a new character set 2924 elif c not in other: 2925 alphabet += 1 # credit for unique characters 2926 other.add(c) 2927 if inset is not lastset: 2928 alphabet += 1 # credit for set transitions 2929 lastset = cset 2930 entropy = len( 2931 string) * math.log(alphabet) / 0.6931471805599453 # math.log(2) 2932 return round(entropy, 2)
2933
2934 2935 -class IS_STRONG(object):
2936 """ 2937 example:: 2938 2939 INPUT(_type='password', _name='passwd', 2940 requires=IS_STRONG(min=10, special=2, upper=2)) 2941 2942 enforces complexity requirements on a field 2943 2944 >>> IS_STRONG(es=True)('Abcd1234') 2945 ('Abcd1234', 2946 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|') 2947 >>> IS_STRONG(es=True)('Abcd1234!') 2948 ('Abcd1234!', None) 2949 >>> IS_STRONG(es=True, entropy=1)('a') 2950 ('a', None) 2951 >>> IS_STRONG(es=True, entropy=1, min=2)('a') 2952 ('a', 'Minimum length is 2') 2953 >>> IS_STRONG(es=True, entropy=100)('abc123') 2954 ('abc123', 'Entropy (32.35) less than required (100)') 2955 >>> IS_STRONG(es=True, entropy=100)('and') 2956 ('and', 'Entropy (14.57) less than required (100)') 2957 >>> IS_STRONG(es=True, entropy=100)('aaa') 2958 ('aaa', 'Entropy (14.42) less than required (100)') 2959 >>> IS_STRONG(es=True, entropy=100)('a1d') 2960 ('a1d', 'Entropy (15.97) less than required (100)') 2961 >>> IS_STRONG(es=True, entropy=100)('añd') 2962 ('a\\xc3\\xb1d', 'Entropy (18.13) less than required (100)') 2963 2964 """ 2965
2966 - def __init__(self, min=None, max=None, upper=None, lower=None, number=None, 2967 entropy=None, 2968 special=None, specials=r'~!@#$%^&*()_+-=?<>,.:;{}[]|', 2969 invalid=' "', error_message=None, es=False):
2970 self.entropy = entropy 2971 if entropy is None: 2972 # enforce default requirements 2973 self.min = 8 if min is None else min 2974 self.max = max # was 20, but that doesn't make sense 2975 self.upper = 1 if upper is None else upper 2976 self.lower = 1 if lower is None else lower 2977 self.number = 1 if number is None else number 2978 self.special = 1 if special is None else special 2979 else: 2980 # by default, an entropy spec is exclusive 2981 self.min = min 2982 self.max = max 2983 self.upper = upper 2984 self.lower = lower 2985 self.number = number 2986 self.special = special 2987 self.specials = specials 2988 self.invalid = invalid 2989 self.error_message = error_message 2990 self.estring = es # return error message as string (for doctest)
2991
2992 - def __call__(self, value):
2993 failures = [] 2994 if value and len(value) == value.count('*') > 4: 2995 return (value, None) 2996 if self.entropy is not None: 2997 entropy = calc_entropy(value) 2998 if entropy < self.entropy: 2999 failures.append(translate("Entropy (%(have)s) less than required (%(need)s)") 3000 % dict(have=entropy, need=self.entropy)) 3001 if type(self.min) == int and self.min > 0: 3002 if not len(value) >= self.min: 3003 failures.append(translate("Minimum length is %s") % self.min) 3004 if type(self.max) == int and self.max > 0: 3005 if not len(value) <= self.max: 3006 failures.append(translate("Maximum length is %s") % self.max) 3007 if type(self.special) == int: 3008 all_special = [ch in value for ch in self.specials] 3009 if self.special > 0: 3010 if not all_special.count(True) >= self.special: 3011 failures.append(translate("Must include at least %s of the following: %s") 3012 % (self.special, self.specials)) 3013 if self.invalid: 3014 all_invalid = [ch in value for ch in self.invalid] 3015 if all_invalid.count(True) > 0: 3016 failures.append(translate("May not contain any of the following: %s") 3017 % self.invalid) 3018 if type(self.upper) == int: 3019 all_upper = re.findall("[A-Z]", value) 3020 if self.upper > 0: 3021 if not len(all_upper) >= self.upper: 3022 failures.append(translate("Must include at least %s upper case") 3023 % str(self.upper)) 3024 else: 3025 if len(all_upper) > 0: 3026 failures.append( 3027 translate("May not include any upper case letters")) 3028 if type(self.lower) == int: 3029 all_lower = re.findall("[a-z]", value) 3030 if self.lower > 0: 3031 if not len(all_lower) >= self.lower: 3032 failures.append(translate("Must include at least %s lower case") 3033 % str(self.lower)) 3034 else: 3035 if len(all_lower) > 0: 3036 failures.append( 3037 translate("May not include any lower case letters")) 3038 if type(self.number) == int: 3039 all_number = re.findall("[0-9]", value) 3040 if self.number > 0: 3041 numbers = "number" 3042 if self.number > 1: 3043 numbers = "numbers" 3044 if not len(all_number) >= self.number: 3045 failures.append(translate("Must include at least %s %s") 3046 % (str(self.number), numbers)) 3047 else: 3048 if len(all_number) > 0: 3049 failures.append(translate("May not include any numbers")) 3050 if len(failures) == 0: 3051 return (value, None) 3052 if not self.error_message: 3053 if self.estring: 3054 return (value, '|'.join(failures)) 3055 from html import XML 3056 return (value, XML('<br />'.join(failures))) 3057 else: 3058 return (value, translate(self.error_message))
3059
3060 3061 -class IS_IN_SUBSET(IS_IN_SET):
3062 3063 REGEX_W = re.compile('\w+') 3064
3065 - def __init__(self, *a, **b):
3066 IS_IN_SET.__init__(self, *a, **b)
3067
3068 - def __call__(self, value):
3069 values = self.REGEX_W.findall(str(value)) 3070 failures = [x for x in values if IS_IN_SET.__call__(self, x)[1]] 3071 if failures: 3072 return (value, translate(self.error_message)) 3073 return (value, None)
3074
3075 3076 -class IS_IMAGE(Validator):
3077 """ 3078 Checks if file uploaded through file input was saved in one of selected 3079 image formats and has dimensions (width and height) within given boundaries. 3080 3081 Does *not* check for maximum file size (use IS_LENGTH for that). Returns 3082 validation failure if no data was uploaded. 3083 3084 Supported file formats: BMP, GIF, JPEG, PNG. 3085 3086 Code parts taken from 3087 http://mail.python.org/pipermail/python-list/2007-June/617126.html 3088 3089 Arguments: 3090 3091 extensions: iterable containing allowed *lowercase* image file extensions 3092 ('jpg' extension of uploaded file counts as 'jpeg') 3093 maxsize: iterable containing maximum width and height of the image 3094 minsize: iterable containing minimum width and height of the image 3095 3096 Use (-1, -1) as minsize to pass image size check. 3097 3098 Examples:: 3099 3100 #Check if uploaded file is in any of supported image formats: 3101 INPUT(_type='file', _name='name', requires=IS_IMAGE()) 3102 3103 #Check if uploaded file is either JPEG or PNG: 3104 INPUT(_type='file', _name='name', 3105 requires=IS_IMAGE(extensions=('jpeg', 'png'))) 3106 3107 #Check if uploaded file is PNG with maximum size of 200x200 pixels: 3108 INPUT(_type='file', _name='name', 3109 requires=IS_IMAGE(extensions=('png'), maxsize=(200, 200))) 3110 """ 3111
3112 - def __init__(self, 3113 extensions=('bmp', 'gif', 'jpeg', 'png'), 3114 maxsize=(10000, 10000), 3115 minsize=(0, 0), 3116 error_message='invalid image'):
3117 3118 self.extensions = extensions 3119 self.maxsize = maxsize 3120 self.minsize = minsize 3121 self.error_message = error_message
3122
3123 - def __call__(self, value):
3124 try: 3125 extension = value.filename.rfind('.') 3126 assert extension >= 0 3127 extension = value.filename[extension + 1:].lower() 3128 if extension == 'jpg': 3129 extension = 'jpeg' 3130 assert extension in self.extensions 3131 if extension == 'bmp': 3132 width, height = self.__bmp(value.file) 3133 elif extension == 'gif': 3134 width, height = self.__gif(value.file) 3135 elif extension == 'jpeg': 3136 width, height = self.__jpeg(value.file) 3137 elif extension == 'png': 3138 width, height = self.__png(value.file) 3139 else: 3140 width = -1 3141 height = -1 3142 assert self.minsize[0] <= width <= self.maxsize[0] \ 3143 and self.minsize[1] <= height <= self.maxsize[1] 3144 value.file.seek(0) 3145 return (value, None) 3146 except: 3147 return (value, translate(self.error_message))
3148
3149 - def __bmp(self, stream):
3150 if stream.read(2) == 'BM': 3151 stream.read(16) 3152 return struct.unpack("<LL", stream.read(8)) 3153 return (-1, -1)
3154
3155 - def __gif(self, stream):
3156 if stream.read(6) in ('GIF87a', 'GIF89a'): 3157 stream = stream.read(5) 3158 if len(stream) == 5: 3159 return tuple(struct.unpack("<HHB", stream)[:-1]) 3160 return (-1, -1)
3161
3162 - def __jpeg(self, stream):
3163 if stream.read(2) == '\xFF\xD8': 3164 while True: 3165 (marker, code, length) = struct.unpack("!BBH", stream.read(4)) 3166 if marker != 0xFF: 3167 break 3168 elif code >= 0xC0 and code <= 0xC3: 3169 return tuple(reversed( 3170 struct.unpack("!xHH", stream.read(5)))) 3171 else: 3172 stream.read(length - 2) 3173 return (-1, -1)
3174
3175 - def __png(self, stream):
3176 if stream.read(8) == '\211PNG\r\n\032\n': 3177 stream.read(4) 3178 if stream.read(4) == "IHDR": 3179 return struct.unpack("!LL", stream.read(8)) 3180 return (-1, -1)
3181
3182 3183 -class IS_UPLOAD_FILENAME(Validator):
3184 """ 3185 Checks if name and extension of file uploaded through file input matches 3186 given criteria. 3187 3188 Does *not* ensure the file type in any way. Returns validation failure 3189 if no data was uploaded. 3190 3191 Arguments:: 3192 3193 filename: filename (before dot) regex 3194 extension: extension (after dot) regex 3195 lastdot: which dot should be used as a filename / extension separator: 3196 True means last dot, eg. file.png -> file / png 3197 False means first dot, eg. file.tar.gz -> file / tar.gz 3198 case: 0 - keep the case, 1 - transform the string into lowercase (default), 3199 2 - transform the string into uppercase 3200 3201 If there is no dot present, extension checks will be done against empty 3202 string and filename checks against whole value. 3203 3204 Examples:: 3205 3206 #Check if file has a pdf extension (case insensitive): 3207 INPUT(_type='file', _name='name', 3208 requires=IS_UPLOAD_FILENAME(extension='pdf')) 3209 3210 #Check if file has a tar.gz extension and name starting with backup: 3211 INPUT(_type='file', _name='name', 3212 requires=IS_UPLOAD_FILENAME(filename='backup.*', 3213 extension='tar.gz', lastdot=False)) 3214 3215 #Check if file has no extension and name matching README 3216 #(case sensitive): 3217 INPUT(_type='file', _name='name', 3218 requires=IS_UPLOAD_FILENAME(filename='^README$', 3219 extension='^$', case=0)) 3220 """ 3221
3222 - def __init__(self, filename=None, extension=None, lastdot=True, case=1, 3223 error_message='enter valid filename'):
3224 if isinstance(filename, str): 3225 filename = re.compile(filename) 3226 if isinstance(extension, str): 3227 extension = re.compile(extension) 3228 self.filename = filename 3229 self.extension = extension 3230 self.lastdot = lastdot 3231 self.case = case 3232 self.error_message = error_message
3233
3234 - def __call__(self, value):
3235 try: 3236 string = value.filename 3237 except: 3238 return (value, translate(self.error_message)) 3239 if self.case == 1: 3240 string = string.lower() 3241 elif self.case == 2: 3242 string = string.upper() 3243 if self.lastdot: 3244 dot = string.rfind('.') 3245 else: 3246 dot = string.find('.') 3247 if dot == -1: 3248 dot = len(string) 3249 if self.filename and not self.filename.match(string[:dot]): 3250 return (value, translate(self.error_message)) 3251 elif self.extension and not self.extension.match(string[dot + 1:]): 3252 return (value, translate(self.error_message)) 3253 else: 3254 return (value, None)
3255
3256 3257 -class IS_IPV4(Validator):
3258 """ 3259 Checks if field's value is an IP version 4 address in decimal form. Can 3260 be set to force addresses from certain range. 3261 3262 IPv4 regex taken from: http://regexlib.com/REDetails.aspx?regexp_id=1411 3263 3264 Arguments: 3265 3266 minip: lowest allowed address; accepts: 3267 str, eg. 192.168.0.1 3268 list or tuple of octets, eg. [192, 168, 0, 1] 3269 maxip: highest allowed address; same as above 3270 invert: True to allow addresses only from outside of given range; note 3271 that range boundaries are not matched this way 3272 is_localhost: localhost address treatment: 3273 None (default): indifferent 3274 True (enforce): query address must match localhost address 3275 (127.0.0.1) 3276 False (forbid): query address must not match localhost 3277 address 3278 is_private: same as above, except that query address is checked against 3279 two address ranges: 172.16.0.0 - 172.31.255.255 and 3280 192.168.0.0 - 192.168.255.255 3281 is_automatic: same as above, except that query address is checked against 3282 one address range: 169.254.0.0 - 169.254.255.255 3283 3284 Minip and maxip may also be lists or tuples of addresses in all above 3285 forms (str, int, list / tuple), allowing setup of multiple address ranges: 3286 3287 minip = (minip1, minip2, ... minipN) 3288 | | | 3289 | | | 3290 maxip = (maxip1, maxip2, ... maxipN) 3291 3292 Longer iterable will be truncated to match length of shorter one. 3293 3294 Examples:: 3295 3296 #Check for valid IPv4 address: 3297 INPUT(_type='text', _name='name', requires=IS_IPV4()) 3298 3299 #Check for valid IPv4 address belonging to specific range: 3300 INPUT(_type='text', _name='name', 3301 requires=IS_IPV4(minip='100.200.0.0', maxip='100.200.255.255')) 3302 3303 #Check for valid IPv4 address belonging to either 100.110.0.0 - 3304 #100.110.255.255 or 200.50.0.0 - 200.50.0.255 address range: 3305 INPUT(_type='text', _name='name', 3306 requires=IS_IPV4(minip=('100.110.0.0', '200.50.0.0'), 3307 maxip=('100.110.255.255', '200.50.0.255'))) 3308 3309 #Check for valid IPv4 address belonging to private address space: 3310 INPUT(_type='text', _name='name', requires=IS_IPV4(is_private=True)) 3311 3312 #Check for valid IPv4 address that is not a localhost address: 3313 INPUT(_type='text', _name='name', requires=IS_IPV4(is_localhost=False)) 3314 3315 >>> IS_IPV4()('1.2.3.4') 3316 ('1.2.3.4', None) 3317 >>> IS_IPV4()('255.255.255.255') 3318 ('255.255.255.255', None) 3319 >>> IS_IPV4()('1.2.3.4 ') 3320 ('1.2.3.4 ', 'enter valid IPv4 address') 3321 >>> IS_IPV4()('1.2.3.4.5') 3322 ('1.2.3.4.5', 'enter valid IPv4 address') 3323 >>> IS_IPV4()('123.123') 3324 ('123.123', 'enter valid IPv4 address') 3325 >>> IS_IPV4()('1111.2.3.4') 3326 ('1111.2.3.4', 'enter valid IPv4 address') 3327 >>> IS_IPV4()('0111.2.3.4') 3328 ('0111.2.3.4', 'enter valid IPv4 address') 3329 >>> IS_IPV4()('256.2.3.4') 3330 ('256.2.3.4', 'enter valid IPv4 address') 3331 >>> IS_IPV4()('300.2.3.4') 3332 ('300.2.3.4', 'enter valid IPv4 address') 3333 >>> IS_IPV4(minip='1.2.3.4', maxip='1.2.3.4')('1.2.3.4') 3334 ('1.2.3.4', None) 3335 >>> IS_IPV4(minip='1.2.3.5', maxip='1.2.3.9', error_message='bad ip')('1.2.3.4') 3336 ('1.2.3.4', 'bad ip') 3337 >>> IS_IPV4(maxip='1.2.3.4', invert=True)('127.0.0.1') 3338 ('127.0.0.1', None) 3339 >>> IS_IPV4(maxip='1.2.3.4', invert=True)('1.2.3.4') 3340 ('1.2.3.4', 'enter valid IPv4 address') 3341 >>> IS_IPV4(is_localhost=True)('127.0.0.1') 3342 ('127.0.0.1', None) 3343 >>> IS_IPV4(is_localhost=True)('1.2.3.4') 3344 ('1.2.3.4', 'enter valid IPv4 address') 3345 >>> IS_IPV4(is_localhost=False)('127.0.0.1') 3346 ('127.0.0.1', 'enter valid IPv4 address') 3347 >>> IS_IPV4(maxip='100.0.0.0', is_localhost=True)('127.0.0.1') 3348 ('127.0.0.1', 'enter valid IPv4 address') 3349 """ 3350 3351 regex = re.compile( 3352 '^(([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])$') 3353 numbers = (16777216, 65536, 256, 1) 3354 localhost = 2130706433 3355 private = ((2886729728L, 2886795263L), (3232235520L, 3232301055L)) 3356 automatic = (2851995648L, 2852061183L) 3357
3358 - def __init__( 3359 self, 3360 minip='0.0.0.0', 3361 maxip='255.255.255.255', 3362 invert=False, 3363 is_localhost=None, 3364 is_private=None, 3365 is_automatic=None, 3366 error_message='enter valid IPv4 address'):
3367 for n, value in enumerate((minip, maxip)): 3368 temp = [] 3369 if isinstance(value, str): 3370 temp.append(value.split('.')) 3371 elif isinstance(value, (list, tuple)): 3372 if len(value) == len(filter(lambda item: isinstance(item, int), value)) == 4: 3373 temp.append(value) 3374 else: 3375 for item in value: 3376 if isinstance(item, str): 3377 temp.append(item.split('.')) 3378 elif isinstance(item, (list, tuple)): 3379 temp.append(item) 3380 numbers = [] 3381 for item in temp: 3382 number = 0 3383 for i, j in zip(self.numbers, item): 3384 number += i * int(j) 3385 numbers.append(number) 3386 if n == 0: 3387 self.minip = numbers 3388 else: 3389 self.maxip = numbers 3390 self.invert = invert 3391 self.is_localhost = is_localhost 3392 self.is_private = is_private 3393 self.is_automatic = is_automatic 3394 self.error_message = error_message
3395
3396 - def __call__(self, value):
3397 if self.regex.match(value): 3398 number = 0 3399 for i, j in zip(self.numbers, value.split('.')): 3400 number += i * int(j) 3401 ok = False 3402 for bottom, top in zip(self.minip, self.maxip): 3403 if self.invert != (bottom <= number <= top): 3404 ok = True 3405 if not (self.is_localhost is None or self.is_localhost == 3406 (number == self.localhost)): 3407 ok = False 3408 if not (self.is_private is None or self.is_private == 3409 (sum([number[0] <= number <= number[1] for number in self.private]) > 0)): 3410 ok = False 3411 if not (self.is_automatic is None or self.is_automatic == 3412 (self.automatic[0] <= number <= self.automatic[1])): 3413 ok = False 3414 if ok: 3415 return (value, None) 3416 return (value, translate(self.error_message))
3417
3418 -class IS_IPV6(Validator):
3419 """ 3420 Checks if field's value is an IP version 6 address. First attempts to 3421 use the ipaddress library and falls back to contrib/ipaddr.py from Google 3422 (https://code.google.com/p/ipaddr-py/) 3423 3424 Arguments: 3425 is_private: None (default): indifferent 3426 True (enforce): address must be in fc00::/7 range 3427 False (forbid): address must NOT be in fc00::/7 range 3428 is_link_local: Same as above but uses fe80::/10 range 3429 is_reserved: Same as above but uses IETF reserved range 3430 is_mulicast: Same as above but uses ff00::/8 range 3431 is_routeable: Similar to above but enforces not private, link_local, 3432 reserved or multicast 3433 is_6to4: Same as above but uses 2002::/16 range 3434 is_teredo: Same as above but uses 2001::/32 range 3435 subnets: value must be a member of at least one from list of subnets 3436 3437 Examples: 3438 3439 #Check for valid IPv6 address: 3440 INPUT(_type='text', _name='name', requires=IS_IPV6()) 3441 3442 #Check for valid IPv6 address is a link_local address: 3443 INPUT(_type='text', _name='name', requires=IS_IPV6(is_link_local=True)) 3444 3445 #Check for valid IPv6 address that is Internet routeable: 3446 INPUT(_type='text', _name='name', requires=IS_IPV6(is_routeable=True)) 3447 3448 #Check for valid IPv6 address in specified subnet: 3449 INPUT(_type='text', _name='name', requires=IS_IPV6(subnets=['2001::/32']) 3450 3451 >>> IS_IPV6()('fe80::126c:8ffa:fe22:b3af') 3452 ('fe80::126c:8ffa:fe22:b3af', None) 3453 >>> IS_IPV6()('192.168.1.1') 3454 ('192.168.1.1', 'enter valid IPv6 address') 3455 >>> IS_IPV6(error_message='bad ip')('192.168.1.1') 3456 ('192.168.1.1', 'bad ip') 3457 >>> IS_IPV6(is_link_local=True)('fe80::126c:8ffa:fe22:b3af') 3458 ('fe80::126c:8ffa:fe22:b3af', None) 3459 >>> IS_IPV6(is_link_local=False)('fe80::126c:8ffa:fe22:b3af') 3460 ('fe80::126c:8ffa:fe22:b3af', 'enter valid IPv6 address') 3461 >>> IS_IPV6(is_link_local=True)('2001::126c:8ffa:fe22:b3af') 3462 ('2001::126c:8ffa:fe22:b3af', 'enter valid IPv6 address') 3463 >>> IS_IPV6(is_multicast=True)('2001::126c:8ffa:fe22:b3af') 3464 ('2001::126c:8ffa:fe22:b3af', 'enter valid IPv6 address') 3465 >>> IS_IPV6(is_multicast=True)('ff00::126c:8ffa:fe22:b3af') 3466 ('ff00::126c:8ffa:fe22:b3af', None) 3467 >>> IS_IPV6(is_routeable=True)('2001::126c:8ffa:fe22:b3af') 3468 ('2001::126c:8ffa:fe22:b3af', None) 3469 >>> IS_IPV6(is_routeable=True)('ff00::126c:8ffa:fe22:b3af') 3470 ('ff00::126c:8ffa:fe22:b3af', 'enter valid IPv6 address') 3471 >>> IS_IPV6(subnets='2001::/32')('2001::8ffa:fe22:b3af') 3472 ('2001::8ffa:fe22:b3af', None) 3473 >>> IS_IPV6(subnets='fb00::/8')('2001::8ffa:fe22:b3af') 3474 ('2001::8ffa:fe22:b3af', 'enter valid IPv6 address') 3475 >>> IS_IPV6(subnets=['fc00::/8','2001::/32'])('2001::8ffa:fe22:b3af') 3476 ('2001::8ffa:fe22:b3af', None) 3477 >>> IS_IPV6(subnets='invalidsubnet')('2001::8ffa:fe22:b3af') 3478 ('2001::8ffa:fe22:b3af', 'invalid subnet provided') 3479 3480 """ 3481
3482 - def __init__( 3483 self, 3484 is_private=None, 3485 is_link_local=None, 3486 is_reserved=None, 3487 is_multicast=None, 3488 is_routeable=None, 3489 is_6to4=None, 3490 is_teredo=None, 3491 subnets=None, 3492 error_message='enter valid IPv6 address'):
3493 self.is_private = is_private 3494 self.is_link_local = is_link_local 3495 self.is_reserved = is_reserved 3496 self.is_multicast = is_multicast 3497 self.is_routeable = is_routeable 3498 self.is_6to4 = is_6to4 3499 self.is_teredo = is_teredo 3500 self.subnets = subnets 3501 self.error_message = error_message
3502
3503 - def __call__(self, value):
3504 try: 3505 import ipaddress 3506 except ImportError: 3507 from gluon.contrib import ipaddr as ipaddress 3508 3509 try: 3510 ip = ipaddress.IPv6Address(value) 3511 ok = True 3512 except ipaddress.AddressValueError: 3513 return (value, translate(self.error_message)) 3514 3515 if self.subnets: 3516 # iterate through self.subnets to see if value is a member 3517 ok = False 3518 if isinstance(self.subnets, str): 3519 self.subnets = [self.subnets] 3520 for network in self.subnets: 3521 try: 3522 ipnet = ipaddress.IPv6Network(network) 3523 except (ipaddress.NetmaskValueError, ipaddress.AddressValueError): 3524 return (value, translate('invalid subnet provided')) 3525 if ip in ipnet: 3526 ok = True 3527 3528 if self.is_routeable: 3529 self.is_private = False 3530 self.is_link_local = False 3531 self.is_reserved = False 3532 self.is_multicast = False 3533 3534 if not (self.is_private is None or self.is_private == 3535 ip.is_private): 3536 ok = False 3537 if not (self.is_link_local is None or self.is_link_local == 3538 ip.is_link_local): 3539 ok = False 3540 if not (self.is_reserved is None or self.is_reserved == 3541 ip.is_reserved): 3542 ok = False 3543 if not (self.is_multicast is None or self.is_multicast == 3544 ip.is_multicast): 3545 ok = False 3546 if not (self.is_6to4 is None or self.is_6to4 == 3547 ip.is_6to4): 3548 ok = False 3549 if not (self.is_teredo is None or self.is_teredo == 3550 ip.is_teredo): 3551 ok = False 3552 3553 if ok: 3554 return (value, None) 3555 3556 return (value, translate(self.error_message))
3557
3558 3559 -class IS_IPADDRESS(Validator):
3560 """ 3561 Checks if field's value is an IP Address (v4 or v6). Can be set to force 3562 addresses from within a specific range. Checks are done with the correct 3563 IS_IPV4 and IS_IPV6 validators. 3564 3565 Uses ipaddress library if found, falls back to PEP-3144 ipaddr.py from 3566 Google (in contrib). 3567 3568 Universal arguments: 3569 3570 minip: lowest allowed address; accepts: 3571 str, eg. 192.168.0.1 3572 list or tuple of octets, eg. [192, 168, 0, 1] 3573 maxip: highest allowed address; same as above 3574 invert: True to allow addresses only from outside of given range; note 3575 that range boundaries are not matched this way 3576 3577 IPv4 specific arguments: 3578 3579 is_localhost: localhost address treatment: 3580 None (default): indifferent 3581 True (enforce): query address must match localhost address 3582 (127.0.0.1) 3583 False (forbid): query address must not match localhost 3584 address 3585 is_private: same as above, except that query address is checked against 3586 two address ranges: 172.16.0.0 - 172.31.255.255 and 3587 192.168.0.0 - 192.168.255.255 3588 is_automatic: same as above, except that query address is checked against 3589 one address range: 169.254.0.0 - 169.254.255.255 3590 is_ipv4: None (default): indifferent 3591 True (enforce): must be an IPv4 address 3592 False (forbid): must NOT be an IPv4 address 3593 3594 IPv6 specific arguments: 3595 3596 is_link_local: Same as above but uses fe80::/10 range 3597 is_reserved: Same as above but uses IETF reserved range 3598 is_mulicast: Same as above but uses ff00::/8 range 3599 is_routeable: Similar to above but enforces not private, link_local, 3600 reserved or multicast 3601 is_6to4: Same as above but uses 2002::/16 range 3602 is_teredo: Same as above but uses 2001::/32 range 3603 subnets: value must be a member of at least one from list of subnets 3604 is_ipv6: None (default): indifferent 3605 True (enforce): must be an IPv6 address 3606 False (forbid): must NOT be an IPv6 address 3607 3608 Minip and maxip may also be lists or tuples of addresses in all above 3609 forms (str, int, list / tuple), allowing setup of multiple address ranges: 3610 3611 minip = (minip1, minip2, ... minipN) 3612 | | | 3613 | | | 3614 maxip = (maxip1, maxip2, ... maxipN) 3615 3616 Longer iterable will be truncated to match length of shorter one. 3617 3618 >>> IS_IPADDRESS()('192.168.1.5') 3619 ('192.168.1.5', None) 3620 >>> IS_IPADDRESS(is_ipv6=False)('192.168.1.5') 3621 ('192.168.1.5', None) 3622 >>> IS_IPADDRESS()('255.255.255.255') 3623 ('255.255.255.255', None) 3624 >>> IS_IPADDRESS()('192.168.1.5 ') 3625 ('192.168.1.5 ', 'enter valid IP address') 3626 >>> IS_IPADDRESS()('192.168.1.1.5') 3627 ('192.168.1.1.5', 'enter valid IP address') 3628 >>> IS_IPADDRESS()('123.123') 3629 ('123.123', 'enter valid IP address') 3630 >>> IS_IPADDRESS()('1111.2.3.4') 3631 ('1111.2.3.4', 'enter valid IP address') 3632 >>> IS_IPADDRESS()('0111.2.3.4') 3633 ('0111.2.3.4', 'enter valid IP address') 3634 >>> IS_IPADDRESS()('256.2.3.4') 3635 ('256.2.3.4', 'enter valid IP address') 3636 >>> IS_IPADDRESS()('300.2.3.4') 3637 ('300.2.3.4', 'enter valid IP address') 3638 >>> IS_IPADDRESS(minip='192.168.1.0', maxip='192.168.1.255')('192.168.1.100') 3639 ('192.168.1.100', None) 3640 >>> IS_IPADDRESS(minip='1.2.3.5', maxip='1.2.3.9', error_message='bad ip')('1.2.3.4') 3641 ('1.2.3.4', 'bad ip') 3642 >>> IS_IPADDRESS(maxip='1.2.3.4', invert=True)('127.0.0.1') 3643 ('127.0.0.1', None) 3644 >>> IS_IPADDRESS(maxip='192.168.1.4', invert=True)('192.168.1.4') 3645 ('192.168.1.4', 'enter valid IP address') 3646 >>> IS_IPADDRESS(is_localhost=True)('127.0.0.1') 3647 ('127.0.0.1', None) 3648 >>> IS_IPADDRESS(is_localhost=True)('192.168.1.10') 3649 ('192.168.1.10', 'enter valid IP address') 3650 >>> IS_IPADDRESS(is_localhost=False)('127.0.0.1') 3651 ('127.0.0.1', 'enter valid IP address') 3652 >>> IS_IPADDRESS(maxip='100.0.0.0', is_localhost=True)('127.0.0.1') 3653 ('127.0.0.1', 'enter valid IP address') 3654 3655 >>> IS_IPADDRESS()('fe80::126c:8ffa:fe22:b3af') 3656 ('fe80::126c:8ffa:fe22:b3af', None) 3657 >>> IS_IPADDRESS(is_ipv4=False)('fe80::126c:8ffa:fe22:b3af') 3658 ('fe80::126c:8ffa:fe22:b3af', None) 3659 >>> IS_IPADDRESS()('fe80::126c:8ffa:fe22:b3af ') 3660 ('fe80::126c:8ffa:fe22:b3af ', 'enter valid IP address') 3661 >>> IS_IPADDRESS(is_ipv4=True)('fe80::126c:8ffa:fe22:b3af') 3662 ('fe80::126c:8ffa:fe22:b3af', 'enter valid IP address') 3663 >>> IS_IPADDRESS(is_ipv6=True)('192.168.1.1') 3664 ('192.168.1.1', 'enter valid IP address') 3665 >>> IS_IPADDRESS(is_ipv6=True, error_message='bad ip')('192.168.1.1') 3666 ('192.168.1.1', 'bad ip') 3667 >>> IS_IPADDRESS(is_link_local=True)('fe80::126c:8ffa:fe22:b3af') 3668 ('fe80::126c:8ffa:fe22:b3af', None) 3669 >>> IS_IPADDRESS(is_link_local=False)('fe80::126c:8ffa:fe22:b3af') 3670 ('fe80::126c:8ffa:fe22:b3af', 'enter valid IP address') 3671 >>> IS_IPADDRESS(is_link_local=True)('2001::126c:8ffa:fe22:b3af') 3672 ('2001::126c:8ffa:fe22:b3af', 'enter valid IP address') 3673 >>> IS_IPADDRESS(is_multicast=True)('2001::126c:8ffa:fe22:b3af') 3674 ('2001::126c:8ffa:fe22:b3af', 'enter valid IP address') 3675 >>> IS_IPADDRESS(is_multicast=True)('ff00::126c:8ffa:fe22:b3af') 3676 ('ff00::126c:8ffa:fe22:b3af', None) 3677 >>> IS_IPADDRESS(is_routeable=True)('2001::126c:8ffa:fe22:b3af') 3678 ('2001::126c:8ffa:fe22:b3af', None) 3679 >>> IS_IPADDRESS(is_routeable=True)('ff00::126c:8ffa:fe22:b3af') 3680 ('ff00::126c:8ffa:fe22:b3af', 'enter valid IP address') 3681 >>> IS_IPADDRESS(subnets='2001::/32')('2001::8ffa:fe22:b3af') 3682 ('2001::8ffa:fe22:b3af', None) 3683 >>> IS_IPADDRESS(subnets='fb00::/8')('2001::8ffa:fe22:b3af') 3684 ('2001::8ffa:fe22:b3af', 'enter valid IP address') 3685 >>> IS_IPADDRESS(subnets=['fc00::/8','2001::/32'])('2001::8ffa:fe22:b3af') 3686 ('2001::8ffa:fe22:b3af', None) 3687 >>> IS_IPADDRESS(subnets='invalidsubnet')('2001::8ffa:fe22:b3af') 3688 ('2001::8ffa:fe22:b3af', 'invalid subnet provided') 3689 """
3690 - def __init__( 3691 self, 3692 minip='0.0.0.0', 3693 maxip='255.255.255.255', 3694 invert=False, 3695 is_localhost=None, 3696 is_private=None, 3697 is_automatic=None, 3698 is_ipv4=None, 3699 is_link_local=None, 3700 is_reserved=None, 3701 is_multicast=None, 3702 is_routeable=None, 3703 is_6to4=None, 3704 is_teredo=None, 3705 subnets=None, 3706 is_ipv6=None, 3707 error_message='enter valid IP address'):
3708 self.minip = minip, 3709 self.maxip = maxip, 3710 self.invert = invert 3711 self.is_localhost = is_localhost 3712 self.is_private = is_private 3713 self.is_automatic = is_automatic 3714 self.is_ipv4 = is_ipv4 3715 self.is_private = is_private 3716 self.is_link_local = is_link_local 3717 self.is_reserved = is_reserved 3718 self.is_multicast = is_multicast 3719 self.is_routeable = is_routeable 3720 self.is_6to4 = is_6to4 3721 self.is_teredo = is_teredo 3722 self.subnets = subnets 3723 self.is_ipv6 = is_ipv6 3724 self.error_message = error_message
3725
3726 - def __call__(self, value):
3727 try: 3728 import ipaddress 3729 except ImportError: 3730 from gluon.contrib import ipaddr as ipaddress 3731 3732 try: 3733 ip = ipaddress.ip_address(value) 3734 except ValueError, e: 3735 return (value, translate(self.error_message)) 3736 3737 if self.is_ipv4 and isinstance(ip, ipaddress.IPv6Address): 3738 retval = (value, translate(self.error_message)) 3739 elif self.is_ipv6 and isinstance(ip, ipaddress.IPv4Address): 3740 retval = (value, translate(self.error_message)) 3741 elif self.is_ipv4 or isinstance(ip, ipaddress.IPv4Address): 3742 retval = IS_IPV4( 3743 minip=self.minip, 3744 maxip=self.maxip, 3745 invert=self.invert, 3746 is_localhost=self.is_localhost, 3747 is_private=self.is_private, 3748 is_automatic=self.is_automatic, 3749 error_message=self.error_message 3750 )(value) 3751 elif self.is_ipv6 or isinstance(ip, ipaddress.IPv6Address): 3752 retval = IS_IPV6( 3753 is_private=self.is_private, 3754 is_link_local=self.is_link_local, 3755 is_reserved=self.is_reserved, 3756 is_multicast=self.is_multicast, 3757 is_routeable=self.is_routeable, 3758 is_6to4=self.is_6to4, 3759 is_teredo=self.is_teredo, 3760 subnets=self.subnets, 3761 error_message=self.error_message 3762 )(value) 3763 else: 3764 retval = (value, translate(self.error_message)) 3765 3766 return retval
3767