App-ZofCMS
view release on metacpan or search on metacpan
lib/App/ZofCMS/Plugin/UserLogin/ForgotPassword.pm view on Meta::CPAN
use Digest::MD5 (qw/md5_hex/);
use HTML::Template;
use MIME::Lite;
sub _key { 'plug_user_login_forgot_password' }
sub _defaults {
return (
dsn => "DBI:mysql:database=test;host=localhost",
user => '',
pass => undef,
opt => { RaiseError => 1, AutoCommit => 1 },
users_table => 'users',
code_table => 'users_forgot_password',
q_code => 'pulfp_code',
max_abuse => '5:10:60', # 5 min intervals, max 10 attempts per 60 min.
min_pass => 6,
code_expiry => 24*60*60, # 1 day
code_length => 6,
use_stage_indicators => 1,
subject => 'Password Reset',
login_page => '/',
button_send_link => q|<input type="submit" class="input_submit"|
. q| value="Send password">|,
button_change_pass => q|<input type="submit" class="input_submit"|
. q| value="Change password">|,
# email_link => undef, # this will be guessed
# from => undef,
# email_template => undef, # use plugin's default template
# no_run => undef
# create_table => undef,
# mime_lite_params => undef,
# email => undef,
);
}
sub _do {
my ( $self, $conf, $t, $q, $config ) = @_;
return
if $conf->{no_run};
$self->{Q_PAGE} = ( $q->{dir} || '' ) . ( $q->{page} || '' );
$self->{CONF} = $conf;
$self->{Q} = $q;
$self->{T} = $t;
create_code_table( $conf )
if $conf->{create_table};
if ( has_value($q->{$conf->{q_code}}) ) {
$self->process_q_code;
}
elsif ( has_value($q->{pulfp_ask_link}) ) {
$self->process_ask_link;
}
else {
$self->process_initial;
}
$t->{t}{plug_forgot_password} = $self->{OUTPUT};
}
sub process_q_code {
my $self = shift;
my $q = $self->{Q};
my $conf = $self->{CONF};
my $code = $q->{ $conf->{q_code} };
my $dbh = $self->dbh;
my $entry = ($dbh->selectall_arrayref(
'SELECT * FROM `' . $conf->{code_table}
. '` WHERE `code` = ?',
{ Slice => {} },
$code,
) || [])->[0];
unless ( $entry ) {
$self->set_stage('code_invalid');
$self->{OUTPUT} = '<p class="reset_code_expired">Your reset'
. ' code has expired. Please try'
. ' resetting your password again.</p>';
return;
}
if ( has_value( $q->{pulfp_has_change_pass} ) ) {
$self->check_change_pass
or return;
$self->set_new_pass( $entry );
$self->set_stage('change_pass_done');
$self->{OUTPUT} = '<p class="reset_pass_success">Your password'
. ' has been successfully'
. ' changed. You can now use it to '
. (
has_value( $conf->{login_page} )
? qq| <a href="$conf->{login_page}">log in</a>|
: ' log in'
)
.'.</p>';
return;
}
else {
$self->set_stage('change_pass_ask');
$self->{OUTPUT} = $self->make_change_pass_form;
}
}
sub set_new_pass {
my $self = shift;
my $entry = shift;
my $q = $self->{Q};
my $conf = $self->{CONF};
my $dbh = $self->dbh;
my $new_pass = md5_hex( $q->{pulfp_pass} );
$dbh->do(
'UPDATE `' . $conf->{users_table}
. '` SET `password` = ? WHERE `login` = ?',
undef,
$new_pass,
$entry->{login},
);
$dbh->do(
'DELETE FROM `' . $conf->{code_table} . '` WHERE `login` = ?',
undef,
$entry->{login},
);
}
sub check_change_pass {
my $self = shift;
my $q = $self->{Q};
my $conf = $self->{CONF};
my ( $pass, $repass ) = @$q{ qw/pulfp_pass pulfp_repass/ };
unless ( has_value( $pass ) and length $pass >= $conf->{min_pass} ) {
$self->set_stage('code_bad_pass_length');
$self->{OUTPUT} = $self->make_change_pass_form(
'Your new password must be at least '
. $conf->{min_pass} . ' characters long.'
);
return;
}
unless ( has_value( $repass ) and $pass eq $repass ) {
$self->set_stage('code_bad_pass_copy');
$self->{OUTPUT} = $self->make_change_pass_form(
'You did not retype your password correctly.'
);
return;
}
return 1;
}
sub make_change_pass_form {
my ( $self, $error ) = @_;
my $q = $self->{Q};
my $conf = $self->{CONF};
my $template = HTML::Template->new_scalar_ref(
\ change_pass_form_template(),
die_on_bad_params => 0,
);
$template->param(
submit_button => $conf->{button_change_pass},
page => $self->{Q_PAGE},
error => $error,
code_name => $conf->{q_code},
code_value => $q->{ $conf->{q_code} },
);
return $template->output;
}
sub process_initial {
my $self = shift;
my $show_form_error = shift;
my $template = HTML::Template->new_scalar_ref(
\ask_login_form_template(),
die_on_bad_params => 0,
);
$template->param(
submit_button => $self->{CONF}{button_send_link},
page => $self->{Q}{page},
$show_form_error ? ( error => $show_form_error ) : (),
);
$self->{OUTPUT} = $template->output;
$self->set_stage('initial');
}
sub process_ask_link {
my $self = shift;
my $q = $self->{Q};
unless ( has_value( $q->{pulfp_login} ) ) {
$self->set_stage('ask_error_login');
$self->process_initial( 'Please specify your login' );
return;
}
my $dbh = $self->dbh;
my $user = (@{
$dbh->selectall_arrayref(
'SELECT * FROM `users` WHERE `login` = ?',
{ Slice => {} },
$q->{pulfp_login},
)
|| [] })[0];
unless ( $user ) {
$self->set_stage('ask_error_no_user');
$self->process_initial(
'The login you provided was not found in the database.'
);
return;
}
if ( $self->check_abuse( $user ) ) {
$self->set_stage('ask_error_abuse');
$self->process_initial(
'Sorry, but due to abuse we have to limit the amount of'
. ' requests. Please wait a short while and try again.'
);
return;
}
my $code = $self->create_access_code( $user );
$self->email_code( $user, $code );
$self->set_stage('emailed');
$self->{OUTPUT} = '<p class="reset_link_send_success">Please check'
. ' your email for further'
. ' instructions on how to reset your password.</p>';
}
sub email_code {
my $self = shift;
my $user = shift;
my $code = shift;
my $conf = $self->{CONF};
my $template = HTML::Template->new(
die_on_bad_params => 0,
ref $conf->{email_template}
? ( filename => ${ $conf->{email_template} } )
: has_value( $conf->{email_template} )
? ( scalarref => \ $conf->{email_template} )
: ( scalarref => \ email_template() )
);
my $link = $conf->{email_link};
unless ( has_value( $link ) ) {
$link = join '', 'http://', @ENV{ qw/SERVER_NAME REQUEST_URI/ };
$link .= $link =~ /\?/ ? '&' : '?';
$link .= $conf->{q_code} . '='
}
$template->param(
'link' => $link . $code,
);
my $msg = MIME::Lite->new (
Subject => $conf->{subject},
( defined $conf->{from} ? ( From => $conf->{from} ) : (), ),
To => (
has_value( $conf->{email} )
? $conf->{email}
: $user->{email}
),
Type => 'text/html',
Data => $template->output,
);
MIME::Lite->send( @{ $conf->{mime_lite_params} } )
if $conf->{mime_lite_params};
$msg->send;
}
sub create_access_code {
my $self = shift;
my $user = shift;
my $conf = $self->{CONF};
my @chars = ( 0 .. 9, 'a'..'z', 'A'..'Z' );
my $code = '';
for ( 0 .. $conf->{code_length} ) {
$code .= $chars[ rand @chars ];
}
my $dbh = $self->dbh;
lib/App/ZofCMS/Plugin/UserLogin/ForgotPassword.pm view on Meta::CPAN
B<Optional>. Takes a string as a value, this will be used as the subject
line of the email sent to the user (the one containing the link to click).
B<Defaults to:> C<Password Reset>
=head3 C<from>
plug_user_login_forgot_password => {
from => undef,
...
plug_user_login_forgot_password => {
from => 'Zoffix Znet <zoffix@cpan.org>',
...
B<Optional>. Takes a scalar as a value that specifies the C<From> field for
your email. If not specified, the plugin will simply not set the C<From>
argument in L<MIME::Lite>'s C<new()> method (which is what this plugin uses
under the hood). See L<MIME::Lite>'s docs for more description.
B<Defaults to:> C<undef> (not specified)
=head3 C<email_link>
plug_user_login_forgot_password => {
email_link => undef, # guess the right page
...
# note how the URI ends with the "invitation" to append the reset
# ... code right to the end
plug_user_login_forgot_password => {
email_link => 'http://foobar.com/your_page?foo=bar&pulfp_code=',
...
B<Optional>. Takes either C<undef> or a string containing a link
as a value. Specifies the link to the page with this plugin enabled, this
link will be emailed to the user so that they could proceed to
enter their new password. When set to C<undef>, the plugin guesses the
current page (using C<%ENV>) and that's what it will use for the link.
If you specify the string, make sure to end it with C<pulfp_code=> (note
the equals sign at the end), where C<pulfp_code> is the value you have set
for C<q_code> argument. B<Defaults to:> C<undef> (makes the plugin guess
the right link)
=head3 C<email_template>
plug_user_login_forgot_password => {
email_template => undef, # use plugin's default template
...
plug_user_login_forgot_password => {
email_template => \'templates/file.tmpl', # read template from file
...
plug_user_login_forgot_password => {
email_template => '<p>Blah blah blah...', # use this string as template
...
B<Optional>. Takes a scalar, a scalar ref, or C<undef> as a value.
Specifies L<HTML::Template> template to use when generating the email
with the reset link. When set to C<undef>, plugin will use its default
template (see OUTPUT section below). If you're using your own template,
the C<link> template variable will contain the link the user needs to follow
(i.e., use C<< <tmpl_var escape='html' name='link'> >>).
B<Defaults to:> C<undef> (plugin's default, see OUTPUT section below)
=head3 C<login_page>
plug_user_login_forgot_password => {
login_page => '/',
...
plug_user_login_forgot_password => {
login_page => '/my-login-page',
...
plug_user_login_forgot_password => {
login_page => 'http://lolwut.com/your-login-page',
...
B<Optional>. As a value, takes either C<undef> or a URI. Once the user is
through will all the stuff plugin wants them to do, the plugin will tell
them that the password has been changed, and that they can no go ahead
and "log in". If C<login_page> is specified, the "log in" text will be
a link pointing to whatever you set in C<login_page>; otherwise, the
"log in" text will be just plain text. B<Defaults to:> C</> (i.e. web root)
=head3 C<mime_lite_params>
plug_user_login_forgot_password => {
mime_lite_params => undef,
...
plug_user_login_forgot_password => {
mime_lite_params => [
'smtp',
'meowmail',
Auth => [ 'FOO/bar', 'p4ss' ],
],
...
B<Optional>. Takes an arrayref or C<undef> as a value.
If specified, the arrayref will be directly dereferenced into
C<< MIME::Lite->send() >>. Here you can set any special send arguments you
need; see L<MIME::Lite> docs for more info. B<Note:> if the plugin refuses
to send email, it could well be that you need to set some
C<mime_lite_params>; on my box, without anything set, the plugin behaves
as if everything went through fine, but no email arrives.
B<Defaults to:> C<undef>
=head3 C<email>
plug_user_login_forgot_password => {
email => undef,
...
plug_user_login_forgot_password => {
email => 'foo@bar.com,meow.cans@catfood.com',
...
B<Optional>. Takes either C<undef> or email address(es) as a value.
This argument tells the plugin where to send the email containing password
reset link. If set to C<undef>, plugin will look into C<users_table> (see
above) and will assume that email address is associated with the user's
account and is stored in the C<email> column of the C<users_table> table.
If you don't want that, set the email address directly here. Note: if you
want to have multiple email addresses, simply separate them with commas.
B<Defaults to:> C<undef> (take emails from C<users_table> table)
=head3 C<button_send_link>
plug_user_login_forgot_password => {
button_send_link => q|<input type="submit" class="input_submit"|
. q| value="Send password">|,
...
B<Optional>. Takes HTML code as a value. This code represents the
submit button in the first form (the one that asks the user to enter
their login). This, for example, allows you to use image buttons instead
of regular ones. Also, feel free to use this as the insertion point
for any extra HTML form you need in this form. B<Defaults to:>
C<< <input type="submit" class="input_submit" value="Send password"> >>
=head3 C<button_change_pass>
plug_user_login_forgot_password => {
button_change_pass => q|<input type="submit" class="input_submit"|
. q| value="Change password">|,
...
B<Optional>. Takes HTML code as a value. This code represents the
submit button in the second form (the one that asks the user to enter
and reconfirm their new password). This, for example, allows you to use
image buttons instead of regular ones. Also, feel free to use this as the
insertion point for any extra HTML form you need in this form.
B<Defaults to:>
C<< <input type="submit" class="input_submit" value="Change password"> >>
=head3 C<no_run>
plug_user_login_forgot_password => {
no_run => undef,
...
plug_user_login_forgot_password => {
no_run => 1,
...
B<Optional>. Takes either true or false values as a value. This
argument is a simple control switch that you can use to tell the plugin
not to execute. If set to a true value, plugin will not run.
B<Defaults to:> C<undef> (for obvious reasons :))
=head3 C<use_stage_indicators>
plug_user_login_forgot_password => {
use_stage_indicators => 1,
...
B<Optional>. Takes either true or false values as a value. When set
to a true value, plugin will set "stage indicators" (see namesake section
below for details); otherwise, it won't set anything. B<Defaults to:> C<1>
=head1 STAGE INDICATORS & PLUGIN'S OUTPUT VARIABLE
All of plugin's output is spit out into a single variable in your
L<HTML::Template> template:
<tmpl_var name='plug_forgot_password'>
This raises the question of controlling the bells and whistles on your
page with regard to what stage the plugin is undergoing
(i.e. is it displaying that form that asks for a login or the one that
is asking the user for a new password?). This is where I<stage indicators>
come into play.
Providing C<use_stage_indicators> argument (see above) is set to a true
value, the plugin will set the key with the name of
appropriate stage indicator to a true value. That key resides in the
C<{t}> ZofCMS Template special key, so that you could use it in your
L<HTML::Template> templates. Possible stage indicators as well as
explanations of when they are set are as follows:
=head2 C<plug_forgot_password_stage_initial>
<tmpl_if name='plug_forgot_password_stage_initial'>
Forgot your pass, huh?
</tmpl_if>
This indicator shows that the plugin is in its initial stage; i.e. the
form asking the user to enter their login is shown.
=head2 C<plug_forgot_password_stage_ask_error_login>
<tmpl_if name='plug_forgot_password_stage_ask_error_login'>
Yeah, that ain't gonna work if you don't tell me your login...
</tmpl_if>
This indicator will be active if the user submits the form that is
asking for his login, but does not specify his login.
=head2 C<plug_forgot_password_stage_ask_error_no_user>
<tmpl_if name='plug_forgot_password_stage_ask_error_no_user'>
Are you sure you got the right address, bro?
</tmpl_if>
This indicator shows that the plugin did not find user's login in the
C<users_table> table.
=head2 C<plug_forgot_password_stage_ask_error_abuse>
<tmpl_if name='plug_forgot_password_stage_ask_error_abuse'>
Give it a rest, idiot!
</tmpl_if>
This indicator shows that the plugin detected abuse (see C<max_abuse>
plugin's argument for details).
=head2 C<plug_forgot_password_stage_emailed>
<tmpl_if name='plug_forgot_password_stage_emailed'>
Sent ya an email, dude!
</tmpl_if>
This indicator turns on when the plugin successfully sent the user
an email containing reset pass link.
=head2 C<plug_forgot_password_stage_code_invalid>
<tmpl_if name='plug_forgot_password_stage_code_invalid'>
Your reset code has expired, buddy. Hurry up, next time!
</tmpl_if>
This indicator is active when the plugin can't find the code the user
is giving it. Under natural circumstances, this will only occur when
the code has expired.
=head2 C<plug_forgot_password_stage_change_pass_ask>
<tmpl_if name='plug_forgot_password_stage_change_pass_ask'>
What's the new pass you want, buddy?
</tmpl_if>
This indicator turns on when the form asking the user for the new password
is active.
=head2 C<plug_forgot_password_stage_code_bad_pass_length>
<tmpl_if name='plug_forgot_password_stage_code_bad_pass_length'>
That pass's too short, dude.
</tmpl_if>
This indicator signals that the user attempted to use too short of a new
password (the length is controlled with the C<min_pass> plugin's argument).
=head2 C<plug_forgot_password_stage_code_bad_pass_copy>
<tmpl_if name='plug_forgot_password_stage_code_bad_pass_copy'>
It's really hard to type the same thing twice, ain't it?
</tmpl_if>
This indicator turns on if the user did not retype the new password
correctly.
=head2 C<plug_forgot_password_stage_change_pass_done>
<tmpl_if name='plug_forgot_password_stage_change_pass_done'>
Well, looks like you're all done with reseting your pass and what not.
</tmpl_if>
This indicator shows that the final stage of plugin's run has been reached;
i.e. the user has successfully reset the password and can go on with
their other business.
=head1 OUTPUT
The plugin generates a whole bunch of various output; what's below should
cover all the bases:
=head2 Default Email Template
<h2>Password Reset</h2>
<p>Hello. Someone (possibly you) requested a password reset. If that
was you, please follow this link to complete the action:
<a href="<tmpl_var escape='html' name='link'>"><tmpl_var escape='html'
name='link'></a></p>
<p>If you did not request anything, simply ignore this email.</p>
You can change this using C<email_template> argument. When using your
own, use C<< <tmpl_var escape='html' name='link'> >> to insert the
link the user needs to follow.
=head2 "Ask Login" Form Template
<form action="" method="POST" id="plug_forgot_password_form">
<div>
<p>Please enter your login into the form below and an email with
further instructions will be sent to you.</p>
<input type="hidden" name="page" value="<tmpl_var escape='html'
name='page'>">
<input type="hidden" name="pulfp_ask_link" value="1">
<tmpl_if name='error'>
<p class="error"><tmpl_var escape='html' name='error'></p>
</tmpl_if>
<label for="pulfp_login">Your login: </label
><input type="text"
class="input_text"
name="pulfp_login"
id="pulfp_login">
<input type="submit"
class="input_submit"
value="Send password">
</div>
</form>
This is the form that asks the user for their login in order to reset
the password. Submit button is plugin's default code, you can control
it with the C<button_send_link> plugin's argument.
=head2 "New Password" Form Template
<form action="" method="POST" id="plug_forgot_password_new_pass_form">
<div>
<p>Please enter your new password.</p>
<input type="hidden" name="page" value="<tmpl_var escape='html'
name='page'>">
<input type="hidden" name="<tmpl_var escape='html'
name='code_name'>"
value="<tmpl_var escape='html' name='code_value'>">
( run in 0.577 second using v1.01-cache-2.11-cpan-5623c5533a1 )