Article
CGI::Application: A Simple, Extensible Web Framework
Introducing CGI::Application::Plugin::ValidateRM
CGI::Application::Plugin::ValidateRM is an aggregation of a number of other modules: Data::FormValidator validates user input using a profile expressed as a Perl data structure; HTML::FillInForm populates HTML forms with data by parsing the HTML itself.
The easiest way to validate the data is to create a method that returns the validation profile:
1 sub _user_profile {
2 my $self = shift;
3 return {
4 required => [qw(username first_name last_name user_id)],
5 optional => [qw(email receive_newsletter)],
6 dependencies => {receive_newsletter => ['email']},
7 dependency_groups => {password_group => [qw/password password2/]},
8 filters => ['trim'],
9 constraints => {
10 email => 'email',
11 username => qr/^\w{6,10}$/,
12 password => [
13 {
14 constraint => qr/^\w{6,10}$/
15 },
16 {
17 name => 'password_mismatch',
18 constraint => sub {
19 my ($password, $password2) = @_;
20 return ($password eq $password2);
21 },
22 params => [qw(password password2)]
23 }
24 ]
25 },
26 msgs => {
27 any_errors => 'some_errors',
28 prefix => 'err_',
29 constraints => {'password_mismatch' =>
"Passwords don't match"}
30 }
31 };
32 }
Line 2: This may not be required if you're not using any object attributes. In this example we aren't, but if we needed to extend validation routine to include database access, then we would require access to the database handle. We'll discuss database access later on.
Line 3: We return the data validation data structure. Note that we could define this in a hash reference in the calling method instead, but we're separating our controller methods from our model methods. If you were very strict, you could place them in separate packages, but I haven't found this to be particularly useful in practice.
Line 3: A list of mandatory fields.
Line 4: A list of optional fields.
Line 5: The email field is optional. However, if the user clicks on the "Receive Newsletter" option, then the email address is required.
Line 6: With a dependency group, if one value is entered, all other values are required.
Lines 7: I typically trim leading and trailing space, as it's very rarely required for my applications. Leave this out if whitespace is important to you.
Lines 9-25: This is a list of more complex constraints.
Line 9: The ValidateRM plugin comes with a set of common format constraints, one of which checks email addresses. To quote the documentation: "[The constraint] checks if the email LOOKS LIKE an email address. This checks if the input contains one @, and a two level domain name. The address portion is checked quite liberally."
Line10: We can use our own regular expressions. We say that a username must contain a minimum of six alphanumeric characters and a maximum of ten. Because I'm trying to illustrate the usefulness of the validation plugin, I've avoided using a more complex expression that would distract from the point I'm trying to get across. You may decide that "123456" should not be a valid username, in which case you may wish to change your regex to "qr/^[a-z]\w{2,5}$/i" so that the first character must be an alpha.
Lines 12-25: You can have multiple constraints per field. The first simply applies the same validation rule to the password that we used for the username.
Lines 16-23: This section illustrates three concepts:
- You can name your constraints. We make use of this later on when we specify custom error messages.
- A constraint can be enforced by an anonymous subroutine. You'll need to do this if your rule is more complex that a simple pattern.
- The
paramsarray supplies the fields that will be passed to the subroutine.
Lines 26-30: Define the messages we will return.
Line 27: If any errors are encountered then the some_errors template variable will be set.
Lines 28: Whenever we encounter a field error, we will pass the error message to the template variable called err_<original_variable_name>.
Lines 29: We supply a custom error message for the "password_mismatch" constraint.
Any form that fails validation is redisplayed with the appropriate messages. By default, an error message of "missing" will be displayed next to a mandatory field where no value has been supplied, and "invalid" will be displayed when the supplied data does not meet our rules. You can change these defaults or use custom error messages. The documentation for Data::FormValidator provides many examples; check out the email lists if you have any questions.
Once we've set up the profile, we'll need to modify our maintain_user method to use it:
1 sub maintain_user {
2 my $self = shift;
3 my $q = $self->query;
4
5 use CGI::Application::Plugin::ValidateRM (qw/check_rm/);
6 my ($results, $err_page) =
7 $self->check_rm('display_user_form', '_user_profile');
8 return $err_page if $err_page;
9
10 if ($q->param('user_id') == 0) {
11 $self->_create_user();
12 } else {
13 $self->_update_user();
14 }
15
16 $self->param('message', 'User saved');
17 $self->display_user_form();
18 }
The new code is found on lines 5-8. We provide the check_rm method the name of our display form method and the name of our profile method.
Now, whenever there's an input error, the user will be presented with a message next to each field that was incorrect, and the form values will be repopulated. And all we had to do was define the rules in a hash reference.
User Input Security
One of Perl's largely unsung features is the taint mode: it assumes that all user input is potentially malicious, and the tainted input may not be used in code that accesses the system environment. While taint mode is obviously useful, we can provide an extra layer of data checking in a validation routine. As illustrated in our validation profile, it's easy to add a regular expression that ensures that the input isn't attempting to inject HTML or JavaScript that might be used in a scripting attack.
Such checks can be quite laborious when coding from scratch, but the burden is greatly reduced with this ValidateRM plugin.
Additional Checks
For the purposes of the discussion, I kept the validation profile simple, but you may want to include some additional rules:
- A username is compulsory when creating a record. It's not required when updating an existing user.
- When updating a user, the password field is optional. If left blank, the original password remains.
- We shouldn't be allowed to create a username that already exists.
If you don't wish to write these checks yourself, you can find them in the code archive that accompanies this tutorial.