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