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