When coding I quite often find myself having to setup some state temporarily limited to just the block I'm in and the routines the block calls.
For way of a simple example, imagine we have a password reset utility on our website. In our example resetting works by sending a user a url with a token in it to email address associated with their account, and then when the user clicks on that url they're sent to a a page where they can send us the token and a new password.
The core of the code to do the actual password resetting might look something like this:
sub reset_password_from_email_token {
my $self = shift;
my $token = shift;
my $password = shift;
# temporarily disable security checks as this user isn't the one
# the session is logged in as
$self->disable_security_checks;
if (any { $_ eq $token } $self->recent_reset_tokens) {
$self->set_password($password);
$self->remove_reset_token($token);
}
# turn the checks back on again
$self->enable_security_checks;
}
This is fairly reasonable code, but prone to subtle bugs.
What if there's a problem with setting the password? For example,
set_password
could have easily been written to raise an exception if the password isn't long enough:
sub set_password {
my $self = shift;
my $value = shift;
if (length($value) {value} = $value;
return $self;
}
And it's fairly reasonable for someone therefore to write something like this:
eval {
$user->reset_password_from_email_token($token, $password);
};
if ($@) {
if ($@ =~ /too short/) {
return render_bad_password_page();
} else { die $@ }
}
return render_password_reset_page();
Have you spotted the problem yet? Yep,
enable_security_checks
never got called.
render_bad_password_page
is running with security off!
What we want is to ensure that security is always turned back on when we exit from
reset_password_from_email_token
no matter how we do that.
We want something like this psudocode:
sub reset_password_from_email_token {
my $self = shift;
my $token = shift;
my $password = shift;
# this user isn't the one the session is logged in as
...temporarily disable security checks somehow...
if (any { $_ eq $token } $self->recent_reset_tokens) {
$self->set_password($password);
$self->remove_reset_token($token);
}
}
Now how to write that? One
very hacky way of doing it would be to localise a state variable that Perl will automatically restore to the original value as it exits the current scope, i.e. as it exits the subroutine:
sub reset_password_from_email_token {
my $self = shift;
my $token = shift;
my $password = shift;
# this user isn't the one the session is logged in as
local $self->{security} = 0;
if (any { $_ eq $token } $self->recent_reset_tokens) {
$self->set_password($password);
$self->remove_reset_token($token);
}
}
Of course, this has several obvious drawbacks. Firstly, it requires the
reset_password_from_email
routine to understand how security works; If we ever change the security implementation of the module we're going to have to alter this code too. Heaven help us if we try this approach on a third party module!
Secondly, it assumes that the implementation of security is sufficiently trivial that it can be controlled by a simple variable. This isn't often the case. You may have to end up localising a whole collection of variables, or even running complex logic to work out what to do. Very very messy.
Quite simply, just resetting variables back to their original state isn't powerful enough of a mechanism. What we actually would like to do is define some
code that will be run on the exit of the subroutine.
One way to do that is to use the
End module from the CPAN:
use End qw(end);
sub reset_password_from_email_token {
my $self = shift;
my $token = shift;
my $password = shift;
# this user isn't the one the session is logged in as
$self->disable_security_checks;
my $temp = end { $self->enable_security_checks };
if (any { $_ eq $token } $self->recent_reset_tokens) {
$self->set_password($password);
$self->remove_reset_token($token);
}
}
As long as
$temp
stays in scope nothing happens, but as soon as the subroutine exits and
$temp
goes out of scope the code we passed in will be executed.
How does that work? The End module is a way to create an instance that runs some code when it's garbage collected. In the above example when
$temp
goes out of scope its DESTROY method will be called which will in turn call the anonymous subroutine that we passed in which calls
enable_security_checks
.
Great! We've solved the problem. We've almost invented a kind of backwards try / catch / finally syntax al-la Java and friends.
The problem with is it still requires me to write code every time I disable security, and therefore think, and therefore have a chance to introduce bugs. What I really really would like to do is write this:
sub reset_password_from_email_token {
my $self = shift;
my $token = shift;
my $password = shift;
# this user isn't the one the session is logged in as
$self->temporarily_disable_security_checks;
if (any { $_ eq $token } $self->recent_reset_tokens) {
$self->set_password($password);
$self->remove_reset_token($token);
}
}
And it to essentially do the same thing, call
disable_security_checks
immediately and
enable_security_checks
at the end of scope. Is this possible? Yes, with the help of another CPAN module,
Scope::Upper:
use Scope::Upper;
sub temporarily_disable_security_checks {
my $self = shift;
# disable security checks immediately
$self->disable_security_checks;
# and when the scope that called us exits, re-enable them
reap sub {
$self->enable_security_checks;
}, UP;
}
Whoa! What happened there? Like
end
, the
reap
function exported by Scope::Upper allows us to to define an anonymous subroutine that will be called when a scope exits - but rather than the
current scope, we can say when
any scope in our call-chain exits. In this example we're saying "UP", which is a constant exported by Scope::Upper which means "in the scope that called us", i.e. run this code when
reset_password_from_email_token
exits.
As you can imagine this is a really powerful mechanism that can be used to encapsulate complex logic. It's useful for all sorts of things from cleanup exercises like I've shown here, to being really helpful in defining new keywords...