rivet/packages/formbroker/formbroker.tcl (670 lines of code) (raw):

# -- formbroker.tcl # # Form validation and sanitation tool. Developed starting from # code initially donated by Karl Lehenbauer (Flightaware.com) # # Copyright 2017 The Rivet Team # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. namespace eval FormBroker { variable form_definitions [dict create] variable form_list [dict create] variable string_quoting force_quote variable form_count 0 # # response_security_error - issue an error with errorCode # # set appropriate -- we expect the rivet error handler # to catch this and do the right thing # proc response_security_error {type message} { error $message "" [list RIVET SECURITY $type $message] } # # force_response_integers - error if any of named vars in response doesn't exist # # or isn't an integer # proc force_response_integers {_response args} { upvar $_response response require_response_vars response {*}$args foreach var $args { if {![regexp {[0-9-]*} response($var)]} { response_security_error NOT_INTEGER "illegal content in $var" } if {![scan $response($var) %d response($var)]} { response_security_error NOT_INTEGER "illegal content in $var" } } } # # force_response_integer_in_range - error if var in response isn't an integer # or if it isn't in range # proc force_response_integer_in_range {_response var lowest highest} { upvar $_response response force_response_integers response $var if {$response($var) < $lowest || $response($var) > $highest} { response_security_error "OUT_OF_RANGE" "$var out of range" } } # -- force_quote # proc force_quote {str} { return "'$str'" } # -- force_sanitize_response_strings proc force_sanitize_response_strings {_response args} { } # # force_quote_response_strings - sanitize and pg_quote all the specified strings in the array # proc force_quote_response_strings {_response args} { upvar $_response response force_sanitize_response_strings response {*}$args foreach var $args { set response($var) [$string_quoting $response($var)] } } # # -- force_quote_response_unfilteredstrings - rewrite named response # elements pg_quoted # proc force_quote_response_unfilteredstrings {_response args} { upvar $_response response require_response_vars response {*}$args foreach var $args { set response($var) [$string_quoting $response($var)] } } # -- base validators # # # -- validate_string # # returned error codes # # - FB_EMPTY_STRING: error condition ignored if # variable descriptor has the 'noempty' flag set # - FB_STRING_TOO_LONG: error condition ignored if # variable descriptor has the 'constrain' flag set # proc validate_string {_var_d} { upvar $_var_d var_d set valid FB_OK dict with var_d { if {$bounds > 0} { if {($nonempty == 1) && ($var == "")} { set valid FB_EMPTY_STRING } elseif {$constrain} { set var [string range $var 0 $bounds-1] } elseif {[string length $var] > $bounds} { set valid FB_STRING_TOO_LONG } } } return $valid } # -- validate_integer # # integer validation checks whether # # 1- the representation *is* an integer # 2- if buonds exist the value must be between [-bound,bound] # 3- if the bounds is a list of 2 elements the value must # be between them # # If needed the variable is constrained within the bounds. # # Returned error codes: # # - FB_OUT_OF_BOUNDS: the integer value lies outside the # bounds set in the variable descriptor. Ths error condition # ignored if the 'constrain' flag is set and the bounded # variable value is returned instead # - NOT_INTEGER: the variable value is not an integer # as checked by the [string is integer <variable value>] # command proc validate_integer {_var_d} { upvar $_var_d var_d #puts "var_d: $var_d" set valid FB_OK dict with var_d { if {![string is integer $var]} { return NOT_INTEGER } if {[llength $bounds] == 2} { ::lassign $bounds min_v max_v if {$constrain} { set var [expr min($var,$max_v)] set var [expr max($var,$min_v)] set valid FB_OK } elseif {($var > $max_v) || ($var < $min_v)} { set valid FB_OUT_OF_BOUNDS } else { set valid FB_OK } } elseif {([llength $bounds] == 1) && ($bounds > 0)} { if {$constrain} { set var [expr min($bounds,$var)] set var [expr max(-$bounds,$var)] set valid FB_OK } elseif {(abs($var) > $bounds)} { set valid FB_OUT_OF_BOUNDS } else { set valid FB_OK } } } return $valid } # -- validate_unsigned # # see validate_integer # # Returned error code: # # - NOT_INTEGER: the variable value is not an integer # as checked by the [string is integer <variable value>] # command # - FB_OUT_OF_BOUNDS: the value is negative or out of # the bounds set in the descriptor # proc validate_unsigned {_var_d} { upvar $_var_d var_d set valid FB_OK dict with var_d { if {![string is integer $var]} { return NOT_INTEGER } if {[llength $bounds] == 2} { ::lassign $bounds min_v max_v if {$constrain} { set var [expr min($var,$max_v)] set var [expr max($var,$min_v)] set valid FB_OK } elseif {($var > $max_v) || ($var < $min_v)} { set valid FB_OUT_OF_BOUNDS } } elseif {([llength $bounds] == 1) && ($bounds > 0)} { if {$constrain} { set var [expr max(0,$var)] set var [expr min($bounds,$var)] set valid FB_OK } elseif {($var > $bounds) || ($var < 0)} { set valid FB_OUT_OF_BOUNDS } } else { if {$constrain} { set var [expr max(0,$var)] set valid FB_OK } elseif {$var < 0} { set valid FB_OUT_OF_BOUNDS } } } return $valid } # -- validate_email # # Returned error code: FB_INVALID_EMAIL # proc validate_email {_var_d} { upvar $_var_d var_d dict with var_d { if {[regexp -nocase {[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}} $var]} { return FB_OK } else { return FB_INVALID_EMAIL } } } # -- validate_boolean # # checks in the variable descriptor whether the variable # representation is a valid boolean representation # # Returned error code: FB_INVALID_BOOLEAN proc validate_boolean {_var_d} { upvar $_var_d var_d dict with var_d { if {[string is boolean $var]} { return FB_OK } else { return FB_INVALID_BOOLEAN } } } proc validate_variable_representation {_var_d} { upvar $_var_d var_d variable form_definitions set validator [dict get $var_d validator] if {[info commands $validator] == ""} { set validator ::FormBroker::validate_string } set validation [$validator var_d] dict set var_d field_validation $validation return [string match $validation FB_OK] } proc validate_var {form_name var_name var_value {force_quoting "-noforcequote"}} { variable form_definitions upvar $var_value value set force_quote_var [string match $force_quoting "-forcequote"] set variable_d [dict get $form_definitions $form_name $var_name] dict set variable_d var $value set valid [validate_variable_representation variable_d] set value [dict get $variable_d var] if {[dict get $variable_d force_quote] || $force_quote_var} { set value [$string_quoting $value] } return $valid } # -- bounds_consistency # # During the form creation stage this method is called # to correct possible inconsistencies with a field bounds # definition # proc bounds_consistency {field_type _bounds} { upvar $_bounds bounds switch $field_type { integer { if {[llength $bounds] == 1} { set bounds [list [expr -abs($bounds)] [expr abs($bounds)]] } elseif {[llength $bounds] > 1} { lassign $bounds l1 l2 set bounds [list [expr min($l1,$l2)] [expr max($l1,$l2)]] } else { set bounds 0 } } unsigned { if {[llength $bounds] == 1} { set bounds [list 0 [expr abs($bounds)]] } elseif {[llength $bounds] > 1} { lassign $bounds l1 l2 if {$l1 < 0} { set l1 0 } if {$l2 < 0} { set l2 0 } set bounds [list [expr min($l1,$l2)] [expr max($l1,$l2)]] } else { set bounds 0 } } } } # -- form_definition # # currently this call returns the dictionary # of form field definitions. It's not meant to be # used in regular development. It's supposed to be # private to the FormBroker package # and it may go away with future developments or # change its interface and returned value proc form_definition {form_name} { variable form_definitions return [dict get $form_definitions $form_name] } # -- validation_error # # returns the result of the last validation # operation called on for this form. # proc validation_error {form_name} { variable form_list return [dict get $form_list $form_name form_validation] } # -- failing # # returns a list of variable-status pairs for each # field in a form that did not validate # proc failing {form_name} { set res {} dict for {field field_d} [form_definition $form_name] { dict with field_d { if {$field_validation != "FB_OK"} { lappend res $field $field_validation } } } return $res } # -- result # # accessor to the form field definitions. This procedure # too is not (at least temporarily) to be called from # outside the package # proc result {form_name form_field} { variable form_definitions return [dict get $form_definitions $form_name $form_field] } # --require_response_vars # # error if any of the specified are not in the response # proc require_response_vars {form_name _response} { upvar $_response response variable form_definitions set missing_vars 0 dict for {var variable_d} [dict get $form_definitions $form_name] { if {![info exists response($var)]} { dict with form_definitions $form_name $var { # if the variable was not in the response # but a default was set then we copy this # value in the variable descriptor and # the response array as well if {[info exists default]} { set response($var) $default set var $default } else { set field_validation MISSING_VAR set missing_vars 1 } } } } if {$missing_vars} { response_security_error MISSING_VAR \ "var $var not present in $_response" } } # -- validate # # proc validate { form_name args } { variable form_definitions variable form_list variable string_quoting set force_quote_vars 0 set arguments $args if {[llength $arguments] == 0} { error "missing required arguments" } elseif {[llength $arguments] > 3} { error "error calling validate, usage: validate ?-forcequote? response ?copy_response?" } while {[llength $arguments]} { set arguments [::lassign $arguments a] if {$a == "-forcequote"} { set force_quote_vars 1 } elseif {![array exists response]} { upvar $a response } else { upvar $a filtered_response array set filtered_response {} } } if {![array exists response]} { error "error calling validate, usage: validate ?-forcequote? response ?copy_response?" } # we now go ahead validating the response variables set form_valid true set vars_to_validate [dict get $form_list $form_name vars] if {[catch { require_response_vars $form_name response } er eopts]} { #puts "$er $eopts" dict set form_list $form_name form_validation FB_MISSING_VARS return false } # field validation dict with form_list $form_name { set form_validation FB_OK } set form_d [dict get $form_definitions $form_name] #puts "form_d: $form_d" array unset response_a dict for {var variable_d} $form_d { dict set variable_d var $response($var) if {[validate_variable_representation variable_d] == 0} { dict set form_list $form_name form_validation FB_VALIDATION_ERROR set form_valid false } else { # in case it was constrained we write the value back # into the response array if {[dict get $variable_d constrain]} { set response_a($var) [dict get $variable_d var] } else { set response_a($var) $response($var) } if {[dict get $variable_d force_quote] || $force_quote_vars} { set response_a($var) [$string_quoting [dict get $variable_d var]] } } dict set form_definitions $form_name $var $variable_d #puts "validated $var -> $variable_d" } # if 'validate' has been called with a filtered_response array # we clean it up and proceed copying the variable values into it if {[array exists filtered_response]} { array unset filtered_response array set filtered_response [array get response_a] } else { array set response [array get response_a] } return $form_valid } # -- response # # proc response {form_name {resp_a response}} { upvar $resp_a response variable form_definitions dict for {var_name var_d} [dict get $form_definitions $form_name] { catch {unset var} catch {unset default} dict with var_d { if {[info exists var]} { set response($var_name) $var } elseif {[info exists default]} { set response($var_name) $default } } } } # -- reset # # proc reset {form_name} { variable form_definitions variable form_list dict set form_list $form_name form_validation FB_OK dict for {var_name var_d} [dict get $form_definitions $form_name] { catch {dict unset var_d $var_name var} } } # -- destroy # # this method is designed to be called # by a 'trace unset' event on the variable # keeping the form description object. # proc destroy {form_name args} { variable form_definitions variable form_list dict unset form_definitions $form_name dict unset form_list $form_name namespace delete ::FormBroker::${form_name} #puts "destroy of $form_name finished" } # -- create # # creates a form object starting from a list of element descriptors # # the procedure accept a list of single descriptors, being each # descriptor a sub-list itself # # - field_name # - type (string, integer, unsigned, email, base64) # - a list of the following keywords and related values # # - bounds <value> # - bounds [low high] # - check_routine [validation routine] # - length [max length] # proc create {args} { variable form_definitions variable form_list variable form_count variable string_quoting set form_name "form${form_count}" incr form_count catch { namespace delete $form_name } namespace eval $form_name { foreach cmd { validate failing \ form_definition \ result validate_var \ destroy validation_error \ response reset } { lappend cmdmap $cmd [list [namespace parent] $cmd [namespace tail [namespace current]]] } namespace ensemble create -map [dict create {*}$cmdmap] unset cmdmap unset cmd } dict set form_definitions $form_name [dict create] dict set form_list $form_name [dict create vars {} \ form_validation FB_OK \ failing {} \ default "" \ quoting $string_quoting] while {[llength $args]} { set args [::lassign $args e] if {$e == "-quoting"} { dict with form_list $form_name { set args [::lassign $args quoting] if {[uplevel [list info proc $quoting]] == ""} { error [list RIVET INVALID_QUOTING_PROC \ "Non existing quoting proc '$quoting'"] } set string_quoting $quoting } continue } # each variable (field) definition must start with the # variable name and variable type. Every other variable # specification argument can be listed in arbitrary order # with the only constraint that argument values must follow # an argument name. If an argument is specified multiple times # the last definition overrides the former ones set e [::lassign $e field_name field_type] # the 'vars' dictionary field stores the # order of form fields in which they are processed # (in general this order would be destroyed by the Tcl's hash # tables) dict with form_list $form_name {::lappend vars $field_name} # this test would handle the case of the most simple possible # variable definition (just the variable name) if {$field_type == ""} { set field_type string } dict set form_definitions $form_name $field_name \ [list type $field_type \ bounds 0 \ constrain 0 \ validator [namespace current]::validate_string \ force_quote 0 \ nonempty 0 \ field_validation FB_OK] dict with form_definitions $form_name $field_name { switch $field_type { integer { set validator [namespace current]::validate_integer } unsigned { set validator [namespace current]::validate_unsigned } email { set validator [namespace current]::validate_email } boolean { set validator [namespace current]::validate_boolean } string - default { set validator [namespace current]::validate_string } } # while {[llength $e] > 0} { set e [::lassign $e field_spec] switch $field_spec { check_routine - validator { set e [::lassign $e validator] } maxlength - bounds { set e [::lassign $e bounds] bounds_consistency $field_type bounds } default { set e [::lassign $e default] # we must not assume the variable 'default' # exists in the dictionary because we # set it only in this code branch dict set form_definitions $form_name $field_name default $default } nonempty { # this flag forces the formbroker to # signal empty strings as form data errors set nonemtpy 1 } constrain { set constrain 1 } noconstrain { set constrain 0 } quote { set force_quote 1 } } } # let's check for possible inconsitencies between # data type and default value. For this purpose # we create a copy of the variable dictionary # representation then we call the validator on it set variable_d [dict get $form_definitions $form_name $field_name] dict set variable_d var $default if {[$validator variable_d] != "FB_OK"} { dict unset form_definitions $form_name $field_name default } } } return [namespace current]::$form_name } # -- form_exists # # check the existence of the form named 'form_name' in the form broker # database. The check is done by simply checking one of dictionaries that # keep the internal database of form definitions proc form_exists {form_command_name} { variable form_definitions return [dict exists $form_definitions [namespace tail $form_command_name]] } proc creategc {varname args} { set formv [uplevel [list set $varname [::FormBroker::create {*}$args]]] uplevel [list trace add variable $varname unset \ [list [namespace current]::destroy [namespace tail $formv]]] return $formv } namespace export * namespace ensemble create } package provide formbroker 1.0.1