Friday, May 4, 2007

Dynamic attribute validation for forms

Developing usable web application is a lot more difficult than many people think. Forms are an obvious point of friction for users, so anything you can do to help get the user through the form successfully is a good thing. One of my applications has a fair amount of restrictions on data that the models will accept (though validation restrictions). Users were frustrated after submitting a form and having it rejected due to failed validations. This presented a couple of challenges.

-There are a couple of AR models that are updated or created from the form

-The validation and feedback to the user should occur as each field is entered, and any error messages should be presented close to the offending field

-The validations on the models are somewhat complex

Here is a flexible, but somewhat expensive, solution I came up with and plopped in a controller. Substitute the AR models in your application for Model1, Model2, Model3 in the following code:

private

def validate_input_by_model
error_msgs = Hash::new
[Model1, Model2, Model3].each do |klass|
classname = klass.to_s.downcase
error_msgs[classname] = Hash::new
if params.has_key?(classname)
obj = klass::new(params[classname])
unless obj.valid?
obj.attributes.each do |k,v|
if params[classname].has_key?(k)
error_msgs[classname][k] = obj.errors.on(k)
end
end
end
end
end
error_msgs
end

public

def validate_field
if request.xml_http_request?
error_msgs = validate_input_by_model
render :update do |page|
error_msgs.each_key do |classname|
error_msgs[classname].each do |k,v|
page.inject_error_msg_if_condition "#{classname}_#{k}", v, v
end
end
end
else
render :nothing => true, :status => 401
end
end

An example of the view code:

<% css_form_for :model1, @model1obj, :url => { :controller => 'controller1', :action => 'confirm' } do |@f| %>
<%= @f.text_field :attribute1, :extra => 'Required' %>
<%= validate_remote_by_model 'model1_attribute1' %>
<% css_fields_for :model2, @model2obj do |@model2_f| %>
<%= @model2_f.text_field :attribute1 %>
<%= validate_remote_by_model 'model2_attribute1' %>
<% end %>
<% end %>

The Helper (for the controller associated views) code:

def validate_remote_by_model(id)
validate_local_by_regex(id, /^[^&]*$/, '&amp; characters are not allowed. ') + observe_field(id, :with => ("%s[%s]" % id.split(/_/)), :url => { :action => 'validate_field' })
end

def validate_local_by_regex(id, regex = nil, error_msg = nil)
default_regex, default_error_msg = *@@validations[@@validation_mapping[id]]
regex ||= default_regex
error_msg ||= default_error_msg
function = <<-SCRIPT
if (value.match(#{regex.inspect}))
{ Element.remove('l_#{id}_error'); }
else
{ var prev_error = document.getElementById('l_#{id}_error');
if (prev_error != null) {
Element.update('l_#{id}_error', '#{error_msg}');
} else {
new Insertion.Before('#{id}', '<div class=\"field_error\" id=\"l_#{id}_error\">#{error_msg}</div>');
}
}
SCRIPT
observe_field(id, :function => function)
end

This code is dense, but the object is to make the view code as simple and clear as possible and still be able to use the pleasant form_for (or css_form_for with plugin) helpers.

The helper code is knarly, since it is generating javascript, but its functionality is pretty simple. The field text is first check in the browser using a regular expression to screen for ampersands (see post about RoR/prototype issue), and then an AJAX call is made to validate_field sending the field and its value formatted in the standard ROR form syntax. The RoR form parameter syntax is model[attribute]=value. The controller code takes over from there and does its magic to see if the field value will pass validation when applied to the corresponding model. If validation fails and error messages are generated, the errors are passed back to the AJAX code. The javascript then inserts a new element in the DOM that is adjacent to the form field and contains the error message. The new element is updated or cleared based on the response to subsequent calls.

You can disregard that *@@validations[@@validation_mapping[id]] reference in validate_local_by_regex. It is simply a dictionary of regular expressions and error messages used for other in-browser validations. It is not used in this case.

Safe model attribute updates using AJAX

Sometimes it is nice to have little AJAX click to edit controls that update specific attributes in a model. There are many ways to implement this functionality, some more DRY than others. The code below references an example application that contains users with a corresponding User model. Once authenticated, the user object is stashed in the session. In the following code, current_user references the instance of User for the currently authenticated user. One might be tempted to do the following in the corresponding controller:

def method_missing(name)
match = /\Aupdate_([a-z]{1,20})\Z/.match(name.id2name)
if match && current_user && current_user.attributes.keys.include?(match[1])
if params[:value]
current_user.update_attribute(match[1],params[:value])
current_user.reload
render :text => current_user.send(match[1])
else
render :nothing => true, :status => 500
end
else
raise NoMethodError, "undefined method '#{name}'"
end
end

This is a good time to point out that care must be taken with the choice of attributes=, update_attribute or update_attributes. attributes= is a good choice, because it pays attention to protect attributes that have been excluded from mass-assignment. Using attr_protected or attr_accessible gives one a consistent mechanism to protect sensitive attributes, which one might assume a user object has. attributes= requires an explicit save call on the object afterwards. update_attribute is probably never a good choice, since it bypasses validations. update_attributes actually just calls attributes= and does the save in one call, so it conforms to the protections provided by attr_protected and attr_accessible. update_attributes is probably the best choice here.

The best solution is to use the attr_protected or attr_accessible mechanisms in your model to restrict sensitive attributes from mass assignment. Since the following reference code uses update_attributes, your attribute restrictions will apply to this update mechanism:

def method_missing(name)
match = /\Aupdate_([a-z]{1,20})\Z/.match(name.id2name)
if match && current_user && current_user.attributes.keys.include?(match[1])
if params[:value]
current_user.update_attributes(match[1] => params[:value])
current_user.reload
render :text => current_user.send(match[1])
else
render :nothing => true, :status => 500
end
else
raise NoMethodError, "undefined method '#{name}'"
end
end

This code is simplified, since the object we need to reference is readily available in the session hash. If you need to pass an object id, you would need to extend the AJAX call (using the :with option for the JavaScriptMacrosHelpers) to include the id as an additional parameter and retrieve the object in the normal way.

Notice this code uses update_attributes and constructs a hash of the attribute name and the value.

In the view, you would have erb such as:

Name: <p id="user_name"><%= h @current_user.name %></p>
<%= in_place_editor 'user_name', {:url => '/user/update_name' } %>

If you feel you need to deviate from the normal attribute protection scheme (you should not as a general principle), then you can do something like this in your model:

def self.protected_attributes
['id','type','salt','hashed_password','verify_code']
end

def safe_attribute?(attr_name)
(self.attributes.keys - User.protected_attributes).include? attr_name
end

and then add it to your method_missing hook like:

def method_missing(name)
match = /\Aupdate_([a-z]{1,20})\Z/.match(name.id2name)
if match&& current_user && current_user.safe_attribute?(match[1])
if params[:value]
current_user.update_attributes(match[1] => params[:value])
current_user.reload
render :text => current_user.send(match[1])
else
render :nothing => true, :status => 500
end
else
raise NoMethodError, "undefined method '#{name}'"
end
end

Hooking method_missing is a little dirty. You might actually need to hook method_missing for other (better) reasons. Here is a solution that is dirty in a different way. In the controller:

private

def called_as
/`([^']*)'/.match(caller[0])[1]
end

public

def update_attr_generic
match = /\Aupdate_([a-z]{1,20})\Z/.match(called_as)
if match && current_user && current_user.attributes.keys.include?(match[1])
if params[:value]
current_user.update_attributes(match[1] => params[:value])
current_user.reload
render :text => current_user.send(match[1])
else
render :nothing => true, :status => 500
end
else
render :nothing => true, :status => 500
end
end

alias_method(update_name, update_attr_generic)
alias_method(update_phone, update_attr_generic)

The obvious down side being that you have to create an aliased method name for each attribute you want the ability to update (code generation?). I am jumping through this naming hoop because, by default, the AJAX stuff included in rails likes to post a single parameter (:value) to a named action (commonly :controller/:action). If you modify the AJAX call to send the attribute name and value, or have a more complicated route map and URI, then you can ditch the method name introspection, regular expression and alias_method foolishness and have a single update_attr method. For example (replace ObjModelClass with the model name):

def update_attr
obj = ObjModelClass.find(params[:id])
if obj && obj.attributes.keys.include?(params[:attrname])
if params[:value]
obj.update_attributes(params[:attrname] => params[:value])
render :text => obj.send(params[:attrname])
else
render :nothing => true, :status => 500
end
else
render :nothing => true, :status => 500
end
end

There you go! A generic attribute updater for a given AR model that adheres to attribute protections placed with attr_protected or attr_accessible. However, you will have to do a little more work in your views to make the AJAX methods pass :id, :attrname and :value parameters to the action. I like to simplify my views, so I often use the dirty, lazy method_missing implementation previously described.

BTW, these methods respond with the value of the attribute after the update, successful or not. If no value is passed in the parameters, the response is an HTTP error code, which most Javascript AJAX libraries handle OK.

Another quick point is that this is pretty generic functionality. All the attribute validation and restrictions should be in your models. Models are the object-oriented part of the RoR MVC implementation. Controllers are the procedural part of your application. Keep your controller code simple. If you find yourself duplicating code in controller actions, it probably can be put into a model (one place to fix and DRY). By using update_attributes or attributes=, you leave the security and enforcement in the model where it belongs.

Go ahead, pick your poison.

Wednesday, January 31, 2007

Speaking of regex and ruby...

I came across this link: http://macromates.com/textmate/manual/regular_expressions

This is a nice summary (with link to original source document) of the regex library used in ruby. Related the UTF-8 regex issues I commented on, the document describes what subset \w actually matches with u extension. It may be out of date (not sure), but it is the most thorough documentation of the ruby regex I have stumbled across.

Ugly bug in prototype/rails

While testing an app, I just noticed that the observe_field (Form.Element.EventObserver), defined this way:

new Form.Element.EventObserver('customer_Name', function(element, value) {new Ajax.Request('/foo/validate_field', {asynchronous:true, evalScripts:true, parameters:'customer[Name]=' + value})})

passed the value through without any munging, which is then parsed by rails into the params hash. Rails param parsing simply splits on & and seems to take the last assignment, so input something like '&amp;' into an input tag with the name 'customer[Name]' will generate:

Processing FooController#validate_field (for 127.0.0.1 at 2007-01-31 17:58:39) [POST]
Session ID: 02685db5b8bfde88f7b858eeec0b3a9d
Parameters: {"action"=>"validate_field", "amp;"=>"", "controller"=>"foo", "customer"=>{"Name"=>""}}
Customer Columns (0.001306) SHOW FIELDS FROM customers
Completed in 0.13547 (7 reqs/sec) | Rendering: 0.02115 (15%) | DB: 0.00131 (0%) | 200 OK [http://127.0.0.1/foo/validate_field]

And input '&customer[Name]=bar' will generate a request like:

Processing FooController#validate_field (for 127.0.0.1 at 2007-01-31 18:16:20) [POST]
Session ID: 02685db5b8bfde88f7b858eeec0b3a9d
Parameters: {"action"=>"validate_field", "controller"=>"foo", "customer"=>{"Name"=>"bar"}}
Customer Columns (0.001054) SHOW FIELDS FROM customers
Completed in 0.13539 (7 reqs/sec) | Rendering: 0.02108 (15%) | DB: 0.00105 (0%) | 200 OK [http://127.0.0.1/foo/validate_field]

Oops. That is not good. Considering the parameters you hope for would include '"customer"=>{"Name"=>"&customer[Name]=bar"}, but instead you get "customer"=>{"Name"=>"bar"}. The value from the field, when parsed by rails, over-wrote the previously generated customer hash. This has a number of implications:

- if you are using an AJAX (prototype) methods to validate form field data, input that includes ampersand(s) will have some or all of the string hidden from the receiver. This assumes that the controller method is trying to receive the data to validate from a specific key(s) in the params hash.

- if you are using AJAX (prototype) methods to receive model-mapped input to update an object, it is possible for someone to piggyback additional attributes (assuming bulk attribute assignment) straight from input fields. No mucking around and hand-crafting a request. Just &model[current_attribute]=data&model[other_attribute]=data_you_did_not_expect. Of course it is still subject to validation and accessible restrictions (you do use those right?). This is not the default BTW. Most of the helpers like observe_field simply send the data from the field as the post body. It will get parsed, and ampersands will cause separate entries in params. It really depends on your expectations and whether you use the :with option to place the data where you expect to find it (or mapped for attribute assignment to an AR object).

- worse I assume. It really depends on a lot of factors. If your models have all attributes accessible to bulk assignment, you use one or more attributes to store information that is state sensitive and you update attributes on that model using AJAX, you may find yourself in trouble.

There are a couple ways to code around this issue, but the right one for you depends greatly on your implementation.