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