Form input validation

A technique for handling form input validation for running an arbitrary number of tests against the submitted data.

Probably doesn’t work with multiple select options

A database table to store all the possible input fields

CREATE TABLE form_items (
 id INT PRIMARY KEY NOT NULL,
 name CHAR(64) NOT NULL,
 type CHAR(6) NOT NULL DEFAULT 'string',
 updated_by CHAR(32),
 updated_at DATETIME
);

There might be additional columns to identify the page or section that an input value is found on.

Then we have a table to identify the validation functions available

CREATE TABLE column_validations (
 id INT PRIMARY KEY NOT NULL,
 name CHAR(32) UNIQUE NOT NULL,
 updated_by CHAR(32),
 updated_at DATETIME
);

There might be other columns to identify the severity and whether to continue or not. in the event of failure

Then we have a many-to-many map of validators to columns

CREATE TABLE column_validation_form_items(
 column_validation_id INT REFERENCES column_validations,
 form_item_id INT REFERENCES form_items
);

A PeeWee model might look like

class BaseModel(Model):
  with app.app_context():
    class Meta:
    database = SqliteDatabase(app.config['DATABASE'])

class form_items(BaseModel):
  id = IntegerField(primary_key=True)
  name = CharField(unique=False)
  type = CharField(default='string')
  updated_by = CharField()
  updated_at = DateTimeField(default=datetime.datetime.now())

class column_validations(BaseModel):
  id = IntegerField(primary_key=True)
  name = CharField(unique=True)
  updated_by = CharField()
  updated_at = DateTimeField(default=datetime.datetime.now())

class column_validation_config_columns(BaseModel):
  column_validation = ForeignKeyField(column_validations)
  form_item = ForeignKeyField(form_items)
  class Meta:
    primary_key = CompositeKey('column_validation', 'form_item')

Sample function to run through the form value validations:

# Take the name of the form field and the submitted value 
 def cleanInput(name, value):
  # Array to map the validator names from the database to the function
  # Not sure if it's possible in Python to do the equivalent of a callback
  # without this type of referencing

  validators = {
    "valid_ip_address": valid_ip_address,
    "valid_ip_range":valid_ip_range,
    "valid_url": valid_url,
    "true_or_false": true_or_false,
    "yes_or_no": yes_or_no,
    "is_integer": is_integer}

  rs = (column_validations.select(column_validations.name)
    .join(column_validation_form_items, JOIN.INNER)
    .join(form_items, JOIN.INNER)
    .where(form_items.name == name))
  # Provide a failsafe for when no validator is found
  # Try each of the validators when the input value in non-blank
  if len(value) > 0:
    status = True
    msg = ""
    for proc in rs:
      func = proc.name
      if func in validators:
        (status, msg) = validators[func](value)
        if not status:
          return (False, "Error: %s validation failure: %s" % (name, msg))
  else: # Empty value, just return True
    return(True, value)
  return (True, msg)

Some sample validation functions. Note that these return a tuple to indicate the status along with an error message. If the status is false, dsiplay the error message and redisplay the form; otherwise continue.

"""
Simple function to validate an IPv4 address
This needs to import socket but note how it avoids regex patterns
Params:
 addr: string containing IP address
Returns:
 (True|False, addr|errmsg)
"""
def valid_ip_address(addr):
  try:
    socket.inet_pton(socket.AF_INET, addr)
    return (True, addr)
  except socket.error:
    return (False, ("Invalid IP address, %s." % addr))

"""
Simple function to validate an IPv4 address range in CIDR format
Pattern match taken from http://blog.markhatton.co.uk/2011/03/15/regular-expressions-for-ip-addresses-cidr-ranges-and-hostnames/
Params:
 ip_range: string containing IP address range
Returns:
 (True|False, ip_range|errmsg)
"""
def valid_ip_range(ip_range):
  patt = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$"
  if re.search(patt, ip_range):
    return (True, ip_range)
  return(False, ("Invalid IPv4 range: %s" % ip_range))

"""
Function to validate a submitted website address
Params:
 addr: String containing a URL
Returns:
 (True|False, url|errmsg)
"""
def valid_url(url):
  if len(url) > 2083:
    return(False, ("Invalid url, %s: too long." % url))
  patt = "(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?"
  if re.search(patt, url, re.IGNORECASE):
    return (True, url)
  return (False, ("Invalid url, %s." % url))

def true_or_false(value):
  for patt in ('true', 'false', '1', '0'):
    if re.search(patt, value, re.IGNORECASE):
      return (True, value)
  return(False, ("%s must be either true or false, 0 or 1" % value))

def yes_or_no(value):
  for patt in ('yes', 'no', '1', '0'):
    if re.search(patt, value, re.IGNORECASE):
      return (True, value)
  return(False, ("%s must be either true or false, 0 or 1" % value))

"""
Simple function to return whether a value is an Integer
Params:
 value: String representation of an integer
Returns:
 (True|False, value|errmsg)
"""
def is_integer(value):
  if isinstance(int(value), int):
    return (True, value)
  return (False, ("%s is not an integer" % value))

References

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.