| # -- 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 |
| |
| 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. |
| # |
| |
| 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 |
| } |
| |
| proc validate_unsigned {_var_d} { |
| upvar $_var_d var_d |
| |
| 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 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 { |
| set valid FB_OK |
| } |
| |
| } else { |
| |
| if {$constrain} { |
| set var [expr max(0,$var)] |
| set valid FB_OK |
| } elseif {$var < 0} { |
| set valid FB_OUT_OF_BOUNDS |
| } else { |
| set valid FB_OK |
| } |
| |
| } |
| } |
| return $valid |
| } |
| |
| 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 |
| } |
| } |
| } |
| |
| proc validate_boolean {_var_d} { |
| upvar $_var_d var_d |
| |
| dict with var_d { |
| if {[string is boolean $var]} { |
| if {$constrain} { |
| set var [string is true $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 |