## FunFormKit, a Webware Form processor ## Copyright (C) 2001, Ian Bicking ## ## This library is free software; you can redistribute it and/or ## modify it under the terms of the GNU Lesser General Public ## License as published by the Free Software Foundation; either ## version 2.1 of the License, or (at your option) any later version. ## ## This library is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## Lesser General Public License for more details. ## ## You should have received a copy of the GNU Lesser General Public ## License along with this library; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## ## NOTE: In the context of the Python environment, I interpret "dynamic ## linking" as importing -- thus the LGPL applies to the contents of ## the modules, but make no requirements on code importing these ## modules. """ Validator/Converters for use with FunFormKit. """ try: from MiscUtils import NoDefault except ImportError: class NoDefault: pass import re, string, socket import cgi try: from mx import DateTime except ImportError: try: import DateTime except ImportError: import sys #sys.stderr.write('Warning: some validators will not work without mxDateTime installed\n') try: from DNS.lazy import mxlookup except ImportError: # Email domain lookup will not be available mxlookup = None from types import * True, False = (1==1), (0==1) def htmlEncode(s): if s is None: return '' return cgi.escape(s, 1) class NotImplementedError(Exception): pass class InvalidField(Exception): pass class ValidatorConverter: """ ValidatorConverter is the (abstract) superclass for various validators and converters. A subclass can validate, convert, or do both. There is no formal distinction made here. ValidatorConverters have two important external methods: * .attemptConvert(value): Attempts to convert the value. If their is a problem, or the value is not valid, an InvalidField exception is raised. The argument for this exception is the (potentially HTML-formatted) error message to give the user for this. * javascript(form_name, field_name, field_description): Returns the javascript code to do client-side validation (but never conversion) of input. Not really used yet, may need to be redesigned. Convenience only, the emitted Javascript should never be relied upon to reject invalid input. There are three important methods for subclasses to override: * __init__(), of course. * .validate(value): This should return an error message if the value is not valid, or None if it is valid. * .convert(value): This returns the converted value, or raises an InvalidField exception if there is an error. The argument to this exception should be the error message. ValidatorConverters should have no internal state besides the values given at instantiation. They should be reusable and reentrant. @@ 2001-04-06 ib: there should be a standard way to give your own error messages if the validators aren't passed. """ def __init__(self, ifInvalid=NoDefault, ifEmpty=NoDefault, messages=None): self._ifInvalid = ifInvalid self._ifEmpty = ifEmpty self._messages = messages pass def message(self, name, default): if not self._messages: return default return self._messages.get(name, default) def attemptConvert(self, value): if self._ifEmpty is not NoDefault and not value: return self._ifEmpty v = self.validate(value) if v: if self._ifInvalid is not NoDefault: return self._ifInvalid else: raise InvalidField, v else: if self._ifInvalid is not NoDefault: try: return self.convert(value) except InvalidField: return self._ifInvalid else: return self.convert(value) def convert(self, value): """By default, all validators do no conversion. You may raise InvalidField in this function.""" return value def javascript(self, form_name, field_name, field_description): """By default there is no JavaScript validation""" return "" def validate(self, value): """Return an error message, or None if valid""" return None class ValidateAny(ValidatorConverter): """This class is like an 'or' operator for validators. The first validator/converter that validates the value will be used. (You can pass in lists of validators, which will be anded) """ def __init__(self, *validators, **kw): if kw.has_key('ifInvalid'): ifInvalid = kw['ifInvalid'] del kw['ifInvalid'] else: ifInvalid = NoDefault assert not kw, 'ValidateAny only takes the ifInvalid keyword argument' if not validators: raise TypeError, 'You must pass at least one validator to ValidateAny' self._validators = validators ValidatorConverter.__init__(self, ifInvalid=ifInvalid, **kw) def attemptConvert(self, value): lastError = None for validatorSet in self._validators: if type(validatorSet) is not type([]) \ and type(validatorSet) is not type(()): validatorSet = [validatorSet] convertedValue = value for validator in validatorSet: try: convertedValue = validator.attemptConvert(value) except InvalidField, v: lastError = v continue return convertedValue raise InvalidField, lastError class ValidateList(ValidatorConverter): """ Use this to apply a validator/converter to each item in a list. For instance: ValidateList(AsInt(), InList([1, 2, 3])) Will take a list of values and try to convert each of them to an integer, and then check if each integer is 1, 2, or 3. """ def __init__(self, *validators, **kw): if kw.has_key('ifInvalid'): ifInvalid==kw['ifInvalid'] del kw['ifInvalid'] else: ifInvalid=NoDefault assert not kw, 'ValidateList only takes the ifInvalid keyword argument' if not validators: raise TypeError, 'You must pass at least one validator to ValidateList' self._validators = validators ValidatorConverter.__init__(self, ifInvalid=ifInvalid, **kw) def attemptConvert(self, valueList): if not type(valueList) is type([]) \ and not type(valueList) is type(()): raise InvalidField, self.message('multipleInputs', 'Expecting multiple inputs') newValue = [] errors = [] for value in valueList: for validator in self._validators: try: value = validator.attemptConvert(value) except InvalidField, v: errors.append('%s: %s' % (htmlEncode(str(value)), v)) continue newValue.append(value) if errors: raise InvalidField, string.join(errors, '
\n') else: return newValue class Constant(ValidatorConverter): """This converter converts everything to the same thing. I.e., you pass in the constant value when initializing, then all values get converted to that constant value. This is only useful with ValidateAny, as in: fromEmailValidator = ValidateAny(ValidEmailAddress(), Constant("unknown@localhost")) In this case, the if the email is not valid "unknown@localhost" will be used instead. """ def __init__(self, value, **kw): self.value = value ValidatorConverter.__init__(self, **kw) def convert(self, value): return self.value class MaxLength(ValidatorConverter): def __init__(self, maxLength, **kw): self._maxLength = maxLength ValidatorConverter.__init__(self, **kw) def validate(self, value): try: if value and \ len(value) > self._maxLength: return self.message('tooLong', "Enter a value less than %(maxLength)i characters long") % {"maxLength": self._maxLength} else: return None except TypeError: return self.message('invalid', "Invalid value") class MinLength(ValidatorConverter): def __init__(self, minLength, **kw): self._minLength = minLength ValidatorConverter.__init__(self, **kw) def validate(self, value): if len(value) < self._minLength: return self.message('tooShort', "Enter a value more than %(minLength)i characters long") % {"minLength": self._minLength} else: return None class NotEmpty(ValidatorConverter): def validate(self, value): if value: return None else: return self.message('empty', "Please enter a value") class Empty(ValidatorConverter): """Useful with ValidateAny""" def validate(self, value): if value: return self.message('notEmpty', "You cannot enter a value here") else: return None class Regex(ValidatorConverter): def __init__(self, regex, strip=False, **kw): if type(regex) is StringType: regex = re.compile(regex) self._regex = regex self._strip = strip ValidatorConverter.__init__(self, **kw) def validate(self, value): if self._strip and type(value) in (StringType, UnicodeType): value = value.strip() if not self._regex.search(value): return self.message('invalid', "The input is not valid") def convert(self, value): if self._strip and type(value) in (StringType, UnicodeType): return value.strip() return value _plaintextRE = re.compile(r"^[a-zA-Z_\-0-9]*$") def PlainText(**kw): if not kw.setdefault('messages', {}).has_key('invalid'): kw['messages']['invalid'] = 'Enter only letters, numbers, or _ (underscore)' return Regex(_plaintextRE, **kw) class InList(ValidatorConverter): def __init__(self, l, allowSublists=False, hideList = False, **kw): """if hideList is true then the list will not be displayed in error messages""" self._list = l self._hideList = hideList self._allowSublists = allowSublists ValidatorConverter.__init__(self, **kw) def validate(self, value): if self._allowSublists and (type(value) is type([]) \ or type(value) is type(())): results = filter(None, map(self.validate, value)) if not results: return None else: return string.join(results, '
') if not value in self._list: if self._hideList: return self.message('invalid', "Invalid value") else: return self.message('notIn', "Value must be one of: %(items)s") % {"items": htmlEncode(string.join(self._list, "; "))} else: return None class DictionaryConverter(ValidatorConverter): """Converts values based on a dictionary which has preprocessed values as keys for the resultant values. If allowNull is passed, it will not balk if a false value (e.g., "" or None) is given (it will return None in these cases). """ def __init__(self, dict, allowNull=True, **kw): self._dict = dict self._allowNull = allowNull ValidatorConverter.__init__(self, **kw) def convert(self, value): if self._allowNull and not value: return None if not self._dict.has_key(value): raise InvalidField, self.message('invalid', "Choose something") else: return self._dict[value] class IndexListConverter(ValidatorConverter): """Converts a index (which may be a string like "2") to the value in the given list.""" def __init__(self, l, **kw): self._list = l ValidatorConverter.__init__(self, **kw) def convert(self, value): try: value = int(value) except ValueError: raise InvalidField, self.message('integer', "Must be an integer index") try: return self._list[value] except IndexError: raise InvalidField, self.message('outOfRange', "Index out of range") class DateValidator(ValidatorConverter): """Be sure to call DateConverter first""" def __init__(self, earliestDate=None, latestDate=None, allowEmpty=False, **kw): self._earliestDate = earliestDate self._latestDate = latestDate self._allowEmpty = allowEmpty ValidatorConverter.__init__(self, **kw) def validate(self, value): if not value and self._allowEmpty: return None if self._earliestDate and value < self._earliestDate: return self.message('after', "Date must be after %(date)s") % \ {"date": self._earliestDate.strftime(self.message('dateFormat', "%A, %d %B %Y"))} if self._latestDate and value > self._latestDate: return self.message('before', "Date must be before %(date)s") % \ {"date": self._latestDate.strftime(self.message('dateFormat', "%A, %d %B %Y"))} return None class AsInt(ValidatorConverter): def convert(self, value): try: return int(value) except ValueError: raise InvalidField, self.message('integer', "Please enter an integer value") class AsNumber(ValidatorConverter): def convert(self, value): try: value = float(value) if value == int(value): return int(value) return value except ValueError: raise InvalidField, self.message('number', "Please enter a number") class AsString(ValidatorConverter): """ Converts things to string, but treats empty things as the empty string. I'm quite sure it doesn't handle every degenerate case. @@: document """ def convert(self, value): if value: return str(value) if value == 0: return str(value) return "" class AsList(ValidatorConverter): """This is for when you think you may return multiple values for a certain field. This way the result will always be a list, even if there's only one result. @@ 2001-04-11 ib: Maybe this should deal with no result ([]) Maybe I'm not sure it works at all. """ def convert(self, value): if not type(value) is type([]): return [value] else: return value class Email(ValidatorConverter): """Validate an email address. If you pass resolveDomain=True, then it will try to resolve the domain name to make sure it's valid. This takes longer, of course. You must have the pyDNS modules installed to look up MX records. """ usernameRE = re.compile(r"^[a-z0-9\_\-']+", re.I) domainRE = re.compile(r"^[a-z0-9\.\-]+\.[a-z]+$", re.I) def __init__(self, resolveDomain=False, **kw): if resolveDomain: assert mxlookup, "pyDNS is not installed on your system (or the DNS package cannot be found). I cannot resolve domain names in addresses" self._resolveDomain = resolveDomain ValidatorConverter.__init__(self, **kw) def validate(self, value): if not value: return self.message('empty', 'Please enter an email address') value = string.strip(value) splitted = string.split(value, '@', 1) if not len(splitted) == 2: return self.message('noAt', 'An email address must contain an @') if not self.usernameRE.search(splitted[0]): return self.message('badUsername', 'The username portion of the email address is invalid (the portion before the @: %(username)s)') % {"username": htmlEncode(splitted[0])} if not self.domainRE.search(splitted[1]): return self.message('badDomain', 'The domain portion of the email address is invalid (the portion after the @: %(domain)s)') % {"domain": htmlEncode(splitted[1])} if self._resolveDomain: domains = mxlookup(splitted[1]) if not domains: return self.message('domainDoesNotExist', 'The domain of the email address does not exist (the portion after the @: %(domain)s)') % {"domain": splitted[1]} return None def convert(self, value): return string.strip(value) class StateProvince(ValidatorConverter): """ Valid state or province code (two-letter). Well, for now I don't know the province codes, but it does state code. """ _states = ['AK', 'AL', 'AR', 'AS', 'AZ', 'CA', 'CO', 'CT', 'DC', 'DE', 'FL', 'FM', 'GA', 'HI', 'IA', 'ID', 'IN', 'IL', 'KS', 'KY', 'LA', 'MA', 'MD', 'ME', 'MH', 'MI', 'MN', 'MO', 'MP', 'MS', 'MT', 'NC', 'ND', 'NE', 'NH', 'NJ', 'NM', 'NV', 'NY', 'OH', 'OK', 'OR', 'PA', 'PR', 'PW', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VA', 'VT', 'WA', 'WI', 'WV', 'WY'] def __init__(self, extraStates=None, **kw): """ extraStates are other postal codes allowed, like provinces or whatnot. If you want an entirely different set of codes, then you should just subclass this and change _states """ ValidatorConverter.__init__(self, **kw) self._extraStates = extraStates def validate(self, value): value = string.strip(string.upper(value)) if not value: return self.message('empty', 'Please enter a state code') if len(value) != 2: return self.message('wrongLength', 'Please enter a state code with TWO letters') if value not in self._states \ and not (self._extraStates and value in self._extraStates): return self.message('invalid', 'That is not a valid state code') return None def convert(self, value): return string.strip(string.upper(value)) class PhoneNumber(ValidatorConverter): """ Validates, and converts to ###-###-####, optionally with extension (as ext.##...) @@: should add internation phone number support """ _phoneRE = re.compile(r'^\s*(?:1-)?(\d\d\d)[\- \.]?(\d\d\d)[\- \.]?(\d\d\d\d)(?:\s*ext\.?\s*(\d+))?\s*$', re.I) def validate(self, value): match = self._phoneRE.search(value) if not match: return self.message('phoneFormat', 'Please enter a number, with area code, in the form ###-###-####, optionally with "ext.####"') def convert(self, value): match = self._phoneRE.search(value) result = '%s-%s-%s' % (match.group(1), match.group(2), match.group(3)) if match.group(4): result = result + " ext.%s" % match.group(4) return result class DateConverter(ValidatorConverter): def __init__(self, **kw): assert DateTime, "You must have mxDateTime installed to use DateConverter" ValidatorConverter.__init__(self, **kw) def convert(self, value): try: value = DateTime.Parser.DateFromString(value, formats=('euro', 'us', 'altus', 'iso', 'altiso', 'lit', 'altlit', 'eurlit')) except: raise InvalidField("Could not understand date") else: return value class DateToTextConverter(ValidatorConverter): def __init__(self, format="%x", **kw): assert DateTime, "You must have mxDateTime installed to use DateToTextConverter" self._format = format ValidatorConverter.__init__(self, **kw) def convert(self, value): try: newValue = value.strftime(self._format) except: raise InvalidField("Could not convert DateTime instance to text using format string '%s'" % self._format) else: return newValue _postalCodeRE = re.compile(r'^\d\d\d\d\d(?:-\d\d\d\d)?$') def PostalCode(**kw): if not kw.setdefault('messages', {}).has_key('invalid'): kw['messages']['invalid'] = 'Please enter a zip code (5 digits)' return Regex(_postalCodeRE, **kw) class FormValidator: """ This is just a skeleton for what an actual form validator should do. The important method is .validate(), of course. It gets passed a dictionary of the (processed) values from the form. If you have .validatePartialForm() return True, then it will get the incomplete values as well -- use .has_key() to test if the field was able to process any particular field. Anyway, .validate() should return a string or a dictionary. If a string, it's an error message that applies to the whole form. If not, then it should be a dictionary of fieldName: errorMessage. The special key "form" is the error message for the form as a whole (i.e., a string is equivalent to {"form": string}). Return None on no errors. """ def __init__(self, messages=None): self._messages = messages def message(self, name, default): if not self._messages: return default return self._messages.get(name, default) def validatePartialForm(self): return False def validatePartial(self, fieldDict): raise NotImplementedError def validate(self, fieldDict): raise NotImplementedError class FieldsMatch(FormValidator): def __init__(self, fieldNames, **kw): assert fieldNames, 'You must give at least one field name (though without two this is boring)' if kw.has_key('showMatch'): self._showMatch = kw['showMatch'] del kw['showMatch'] else: self._showMatch = False self._fieldNames = fieldNames FormValidator.__init__(self, **kw) def validatePartialForm(self): return True def validatePartial(self, dict): for name in self._fieldNames: if not dict.has_key(name): return None return self.validate(dict) def validate(self, dict): ref = dict.get(self._fieldNames[0], '') errors = {} for name in self._fieldNames[1:]: if dict.get(name, '') != ref: if self._showMatch: errors[name] = self.message('invalid', "Fields do not match (should be %(match)s)") % {"match": ref} else: errors[name] = self.message('invalidNoMatch', "Fields do not match") return errors class CreditCardValidator(FormValidator): """ Checks that credit card numbers are valid (if not real). You pass in the name of the field that has the credit card type and the field with the credit card number. The credit card type should be one of "visa", "mastercard", "amex", "dinersclub", "discover", "jcb". You must check the expiration date yourself (there is no relation between CC number/types and expiration dates). """ def __init__(self, ccTypeField, ccNumberField, **kw): self._ccTypeField = ccTypeField self._ccNumberField = ccNumberField FormValidator.__init__(self, **kw) def validatePartialForm(self): return True def validatePartial(self, fieldDict): if not fieldDict.get(self._ccTypeField, None) \ or not fieldDict.get(self._ccNumberField, None): return None return self.validate(fieldDict) def validate(self, fieldDict): ccType = string.lower(string.strip(fieldDict[self._ccTypeField])) number = string.strip(fieldDict[self._ccNumberField]) number = string.replace(number, ' ', '') number = string.replace(number, '-', '') try: long(number) except ValueError: return {self._ccNumberField: self.message('invalidNumber', "Please enter only the number, no other characters")} assert _cardInfo.has_key(ccType), "I can't validate that type of credit card" foundValid = False validLength = False for prefix, length in _cardInfo[ccType]: if len(number) == length: validLength = True if len(number) == length \ and number[:len(prefix)] != prefix: foundValid = True break if not validLength: return {self._ccNumberField: self.message('badLength', "You did not enter a valid number of digits")} if not foundValid: return {self._ccNumberField: self.message('invalidNumber', "That number is not valid")} if not _validateMod10(number): return {self._ccNumberField: self.message('invalidNumber', "That number is not valid")} return None def _validateMod10(s): double = 0 sum = 0 for i in range(len(s) - 1, -1, -1): for c in str((double + 1) * int(s[i])): sum = sum + int(c) double = (double + 1) % 2 return((sum % 10) == 0) _cardInfo = { "visa": [('4', 16), ('4', 13)], "mastercard": [('51', 16), ('52', 16), ('53', 16), ('54', 16), ('55', 16)], "discover": [('6011', 16)], "amex": [('34', 15), ('37', 15)], "dinersclub": [('300', 14), ('301', 14), ('302', 14), ('303', 14), ('304', 14), ('305', 14), ('36', 14), ('38', 14)], "jcb": [('3', 16), ('2131', 15), ('1800', 15)], }