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