diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt
index 57483294b..ebeae6412 100644
--- a/docs/changelog/7.x.x.txt
+++ b/docs/changelog/7.x.x.txt
@@ -42,6 +42,8 @@
- fix: IP addresses for adminModeSubnets not using X-Forwarded-For properly
- The Events Calendar is now the new Calendar with some fun new features.
All your existing Events Calendars will be migrated automatically.
+ - Major change: password recovery is now based on profile fields rather than
+ email account access
*** PLEASE READ THE GOTCHAS ***
7.2.3
diff --git a/docs/gotcha.txt b/docs/gotcha.txt
index 054af3a64..21ceb2e75 100644
--- a/docs/gotcha.txt
+++ b/docs/gotcha.txt
@@ -14,6 +14,16 @@ save you many hours of grief.
running the entire test suite prior to SVN commits easier to do
since it won't take so long.
+ * Password recovery has been redone. It is now based on profile fields
+ rather than email access. Since there's no real way to migrate the
+ latter to one to the other, this upgrade disables password recovery;
+ before enabling it again, use the profile fields editor to set certain
+ fields as required for password recovery. Then any user who enters all
+ of those fields correctly can recover their password. The template
+ variables are also different, so if you have a custom password recovery
+ template, you will have to update it. See the new default password
+ recovery template for an example of how to use the new variables.
+
7.2.0
--------------------------------------------------------------------
* NOTE: if you tried to upgrade to 7.2.0 and it failed during the
diff --git a/docs/upgrades/templates-7.3.0/default-password-recovery-template.tmpl b/docs/upgrades/templates-7.3.0/default-password-recovery-template.tmpl
new file mode 100644
index 000000000..8270df0bb
--- /dev/null
+++ b/docs/upgrades/templates-7.3.0/default-password-recovery-template.tmpl
@@ -0,0 +1,47 @@
+#PBtmpl0000000000000014
+#namespace:Auth/WebGUI/Recovery2
+
+
+
+
+
+
+
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+
+
+ |
+ |
+
+
+
+
+ |
+ |
+
+
+
+
+ |
+ |
+
+
+
+
+
diff --git a/docs/upgrades/upgrade_7.2.3-7.3.0.pl b/docs/upgrades/upgrade_7.2.3-7.3.0.pl
index 258dd95c9..3fef66349 100644
--- a/docs/upgrades/upgrade_7.2.3-7.3.0.pl
+++ b/docs/upgrades/upgrade_7.2.3-7.3.0.pl
@@ -23,6 +23,7 @@ addWikiAssets($session);
deleteOldFiles($session);
addFileFieldsToDataForm($session);
makeRSSFromParentAlwaysHidden($session);
+addProfileFieldsOnPasswordRecovery($session);
addNewCalendar($session);
migrateCalendars($session);
removeOldCalendar($session);
@@ -278,6 +279,20 @@ sub removeOldCalendar {
$session->config->deleteFromArray("assets","WebGUI::Asset::Wobject::EventsCalendar");
}
+#-------------------------------------------------
+sub addProfileFieldsOnPasswordRecovery {
+ my $session = shift;
+ print "\tAdding requiredForPasswordRecovery to userProfileField rows.\n" unless $quiet;
+ $session->db->write($_) for(<<'EOT',
+ ALTER TABLE userProfileField
+ ADD COLUMN requiredForPasswordRecovery int(11) NOT NULL default '0'
+EOT
+ );
+
+ $session->setting->set('webguiPasswordRecovery', 0);
+ $session->setting->add('webguiPasswordRecoveryRequireUsername', 1);
+ $session->setting->set('webguiPasswordRecoveryTemplate', 'PBtmpl0000000000000014');
+}
# ---- DO NOT EDIT BELOW THIS LINE ----
diff --git a/lib/WebGUI/Auth/WebGUI.pm b/lib/WebGUI/Auth/WebGUI.pm
index fcca836d3..8f566280f 100644
--- a/lib/WebGUI/Auth/WebGUI.pm
+++ b/lib/WebGUI/Auth/WebGUI.pm
@@ -55,7 +55,7 @@ sub _hasMixedCaseCharacters {
=head2 _isValidPassword ( )
- Validates the password9.
+ Validates the password.
=cut
@@ -427,11 +427,11 @@ sub editUserSettingsForm {
-value=>$self->session->setting->get("webguiPasswordRecovery"),
-label=>$i18n->get(6)
);
- $f->textarea(
- -name=>"webguiRecoverPasswordEmail",
- -label=>$i18n->get(134, 'WebGUI'),
- -value=>$self->session->setting->get("webguiRecoverPasswordEmail")
- );
+ $f->yesNo(
+ -name=>"webguiPasswordRecoveryRequireUsername",
+ -value=>$self->session->setting->get("webguiPasswordRecoveryRequireUsername"),
+ -label=>$i18n->get('require username for password recovery')
+ );
$f->yesNo(
-name=>"webguiValidateEmail",
-value=>$self->session->setting->get("webguiValidateEmail"),
@@ -469,7 +469,7 @@ sub editUserSettingsForm {
$f->template(
-name=>"webguiPasswordRecoveryTemplate",
-value=>$self->session->setting->get("webguiPasswordRecoveryTemplate"),
- -namespace=>"Auth/WebGUI/Recovery",
+ -namespace=>"Auth/WebGUI/Recovery2",
-label=>$i18n->get("password recovery template")
);
return $f->printRowsOnly;
@@ -491,7 +491,7 @@ sub editUserSettingsFormSave {
$s->set("webguiChangeUsername", $f->process("webguiChangeUsername","yesNo"));
$s->set("webguiChangePassword", $f->process("webguiChangePassword","yesNo"));
$s->set("webguiPasswordRecovery", $f->process("webguiPasswordRecovery","yesNo"));
- $s->set("webguiRecoverPasswordEmail", $f->process("webguiRecoverPasswordEmail","textarea"));
+ $s->set("webguiPasswordRecoveryRequireUsername", $f->process("webguiPasswordRecoveryRequireUsername","yesNo"));
$s->set("webguiValidateEmail", $f->process("webguiValidateEmail","yesNo"));
$s->set("webguiUseCaptcha", $f->process("webguiUseCaptcha","yesNo"));
$s->set("webguiAccountTemplate", $f->process("webguiAccountTemplate","template"));
@@ -570,63 +570,140 @@ sub new {
#-------------------------------------------------------------------
sub recoverPassword {
my $self = shift;
- return $self->displayLogin if($self->userId ne "1");
- my $template = 'Auth/WebGUI/Recovery';
- my $vars;
+ return $self->displayLogin unless $self->session->setting->get('webguiPasswordRecovery') and $self->userId eq '1';
+ my @fields = @{WebGUI::ProfileField->getPasswordRecoveryFields($self->session)};
+ return $self->displayLogin unless @fields;
+
+ my $vars = {};
my $i18n = WebGUI::International->new($self->session);
$vars->{title} = $i18n->get(71);
- $vars->{'recover.form.header'} = "\n\n".WebGUI::Form::formHeader($self->session,{});
- $vars->{'recover.form.hidden'} = WebGUI::Form::hidden($self->session,{"name"=>"op","value"=>"auth"});
- $vars->{'recover.form.hidden'} .= WebGUI::Form::hidden($self->session,{"name"=>"method","value"=>"recoverPasswordFinish"});
+ $vars->{'recoverFormHeader'} = "\n\n".WebGUI::Form::formHeader($self->session,{});
+ $vars->{'recoverFormHidden'} = WebGUI::Form::hidden($self->session,{"name"=>"op","value"=>"auth"});
+ $vars->{'recoverFormHidden'} .= WebGUI::Form::hidden($self->session,{"name"=>"method","value"=>"recoverPasswordFinish"});
- $vars->{'recover.form.submit'} = WebGUI::Form::submit($self->session,{});
- $vars->{'recover.form.footer'} = WebGUI::Form::formFooter($self->session,);
- $vars->{'login.url'} = $self->session->url->page('op=auth;method=init');
- $vars->{'login.label'} = $i18n->get(58);
+ $vars->{'recoverFormSubmit'} = WebGUI::Form::submit($self->session,{});
+ $vars->{'recoverFormFooter'} = WebGUI::Form::formFooter($self->session,);
+ $vars->{'loginUrl'} = $self->session->url->page('op=auth;method=init');
+ $vars->{'loginLabel'} = $i18n->get(58);
+
+ $vars->{'anonymousRegistrationIsAllowed'} = ($self->session->setting->get("anonymousRegistration"));
+ $vars->{'createAccountUrl'} = $self->session->url->page('op=auth;method=createAccount');
+ $vars->{'createAccountLabel'} = $i18n->get(67);
+ $vars->{'recoverMessage'} = $_[0] if ($_[0]);
+
+ # Semi-duplication with WebGUI::Auth::createAccount. -.-
+ $vars->{'recoverFormProfile'} = [];
+ foreach my $field (@fields) {
+ my ($id, $formField, $label) = ($field->getId, $field->formField, $field->getLabel);
+ push @{$vars->{'recoverFormProfile'}},
+ +{ 'id' => $id, 'formElement' => $formField, 'label' => $label };
+
+ my $prefix = 'recoverFormProfileField' . ucfirst($id);
+ $vars->{$prefix.'FormElement'} = $formField;
+ $vars->{$prefix.'Label'} = $label;
+ }
+
+ if ($self->getSetting('passwordRecoveryRequireUsername')) {
+ $vars->{'recoverFormUsername'} = WebGUI::Form::text($self->session, {name => 'authWebGUI.username'});
+ $vars->{'recoverFormUsernameLabel'} = $i18n->get(50);
+ }
- $vars->{'anonymousRegistration.isAllowed'} = ($self->session->setting->get("anonymousRegistration"));
- $vars->{'createAccount.url'} = $self->session->url->page('op=auth;method=createAccount');
- $vars->{'createAccount.label'} = $i18n->get(67);
- $vars->{'recover.message'} = $_[0] if ($_[0]);
- $vars->{'recover.form.email'} = WebGUI::Form::text($self->session,{"name"=>"email"});
- $vars->{'recover.form.email.label'} = $i18n->get(56);
return WebGUI::Asset::Template->new($self->session,$self->getPasswordRecoveryTemplateId)->process($vars);
}
#-------------------------------------------------------------------
sub recoverPasswordFinish {
- my $self = shift;
+ my $self = shift;
my $i18n = WebGUI::International->new($self->session);
- return $self->recoverPassword('') if ($self->session->form->process("email") eq "");
- return $self->displayLogin unless ($self->session->setting->get("webguiPasswordRecovery"));
-
- my($sth,$username,$userId,$password,$flag,$message,$output,$encryptedPassword,$authMethod);
- $sth = $self->session->db->read("select users.username,users.userId from users, userProfileData where users.userId=userProfileData.userId and
- users.authMethod='WebGUI' and userProfileData.fieldName='email' and userProfileData.fieldData=".$self->session->db->quote($self->session->form->process("email")));
- $flag = 0;
- while (($username,$userId) = $sth->array) {
- my $len = $self->session->setting->get("webguiPasswordLength") || 6;
- $password = "";
- for(my $i = 0; $i < $len; $i++) {
- $password .= chr(ord('A') + randint(32));
- }
- $encryptedPassword = Digest::MD5::md5_base64($password);
- $self->saveParams($userId,"WebGUI",{identifier=>$encryptedPassword});
- $self->_logSecurityMessage();
- $self->session->errorHandler->security("recover a password. Password emailed to: ".$self->session->form->process("email"));
- $message = $self->session->setting->get("webguiRecoverPasswordEmail");
- $message .= "\n".$i18n->get(50).": ".$username."\n";
- $message .= $i18n->get(51).": ".$password."\n";
- my $mail = WebGUI::Mail::Send->create($self->session, {to=>$self->session->form->process("email"),subject=>$i18n->get(74)});
- $mail->addText($message);
- $mail->addFooter;
- $mail->send;
- $flag++;
+ my $i18n2 = WebGUI::International->new($self->session, 'AuthWebGUI');
+ return $self->displayLogin unless $self->session->setting->get('webguiPasswordRecovery') and $self->userId eq '1';
+
+ my $username;
+ if ($self->getSetting('passwordRecoveryRequireUsername')) {
+ $username = $self->session->form->process('authWebGUI.username');
+ return $self->recoverPassword($i18n2->get('password recovery no username')) unless defined $username;
+ }
+
+ my @fields = @{WebGUI::ProfileField->getPasswordRecoveryFields($self->session)};
+ return $self->displayLogin unless @fields;
+
+ my %fieldValues;
+ my @failedRequiredFields;
+ foreach my $field (@fields) {
+ my $value = $field->formProcess;
+ $fieldValues{$field->getId} = $value;
+ push @failedRequiredFields, $field unless defined $value;
+ }
+
+ if (@failedRequiredFields) {
+ my $errorMessage = '' . join("\n", map {
+ '- ' . $_->getLabel . ' ' . $i18n->get(451) . '
'
+ } @failedRequiredFields) . '
';
+ return $self->recoverPassword($errorMessage);
+ }
+
+ my @fieldNames = keys %fieldValues;
+ my @fieldValues = values %fieldValues;
+ my $joins = join(' ', map{"INNER JOIN userProfileData AS p$_ ON u.userId = p$_.userId AND p$_.fieldName = ".$self->session->db->quote($fieldNames[$_])} (0..$#fieldNames));
+ my $wheres = join(' ', map{"AND p$_.fieldData = ?"} (0..$#fieldNames));
+ $wheres .= ' AND u.username = ?' if defined $username;
+ my $sql = "SELECT u.userId FROM users AS u $joins WHERE u.authMethod = 'WebGUI' $wheres";
+ my @userIds = $self->session->db->buildArray($sql, [@fieldValues, (defined($username)? ($username) : ())]);
+
+ if (@userIds == 0) {
+ return $self->recoverPassword($i18n2->get('password recovery no results'));
+ } elsif (@userIds > 1) {
+ return $self->recoverPassword($i18n2->get('password recovery multiple results'));
+ }
+
+ # Exactly one result.
+ my $userId = $userIds[0];
+ my ($password, $passwordConfirm) = ($self->session->form->process('authWebGUI.identifier'), $self->session->form->process('authWebGUI.identifierConfirm'));
+
+ unless (defined $password and defined $passwordConfirm) {
+ my $vars = {};
+ $vars->{title} = $i18n->get(71);
+ $vars->{'recoverFormHeader'} = "\n\n" . WebGUI::Form::formHeader($self->session, {});
+ $vars->{'recoverFormHidden'} =
+ (WebGUI::Form::hidden($self->session, {name => 'op', value => 'auth'})
+ . WebGUI::Form::hidden($self->session, {name => 'method', value => 'recoverPasswordFinish'})
+ . (defined($username)?
+ WebGUI::Form::hidden($self->session, {name => 'authWebGUI.username',
+ value => $username}) : '')
+ . join('', map{WebGUI::Form::hidden($self->session, {name => $_, value => $fieldValues{$_}})}
+ keys %fieldValues));
+ $vars->{'recoverFormSubmit'} = WebGUI::Form::submit($self->session, {});
+ $vars->{'recoverFormFooter'} = WebGUI::Form::formFooter($self->session);
+
+ # Duplication with above in recoverPassword.
+ $vars->{'loginUrl'} = $self->session->url->page('op=auth;method=init');
+ $vars->{'loginLabel'} = $i18n->get(58);
+
+ $vars->{'anonymousRegistrationIsAllowed'} = ($self->session->setting->get("anonymousRegistration"));
+ $vars->{'createAccountUrl'} = $self->session->url->page('op=auth;method=createAccount');
+ $vars->{'createAccountLabel'} = $i18n->get(67);
+ # End duplication.
+
+ $vars->{'recoverFormPassword'} = WebGUI::Form::password($self->session, {name => 'authWebGUI.identifier'});
+ $vars->{'recoverFormPasswordConfirm'} = WebGUI::Form::password($self->session, {name => 'authWebGUI.identifierConfirm'});
+ $vars->{'recoverFormPasswordLabel'} = $i18n->get(51);
+ $vars->{'recoverFormPasswordConfirmLabel'} = $i18n2->get(2);
+
+ # Mrgh. z.z
+ $vars->{'doingRecovery'} = 1;
+ return WebGUI::Asset::Template->new($self->session, $self->getPasswordRecoveryTemplateId)->process($vars);
+ }
+
+ if ($self->_isValidPassword($password, $passwordConfirm)) {
+ $self->user(WebGUI::User->new($self->session, $userId));
+ $self->saveParams($userId, $self->authMethod,
+ { identifier => Digest::MD5::md5_base64($password),
+ passwordLastUpdated => $self->session->datetime->time });
+ $self->_logSecurityMessage;
+ return $self->SUPER::login;
+ } else {
+ return $self->recoverPassword('');
}
- $sth->finish();
-
- return $self->displayLogin('') if($flag);
- return $self->recoverPassword('');
}
#-------------------------------------------------------------------
diff --git a/lib/WebGUI/Operation/ProfileSettings.pm b/lib/WebGUI/Operation/ProfileSettings.pm
index ac0b4492d..5e40e43bd 100644
--- a/lib/WebGUI/Operation/ProfileSettings.pm
+++ b/lib/WebGUI/Operation/ProfileSettings.pm
@@ -272,6 +272,12 @@ sub www_editProfileField {
-hoverHelp => $i18n->get('showAtRegistration hoverHelp'),
-value => $data->{showAtRegistration}
);
+ $f->yesNo(
+ -name => 'requiredForPasswordRecovery',
+ -label => $i18n->get('requiredForPasswordRecovery label'),
+ -hoverHelp => $i18n->get('requiredForPasswordRecovery hoverHelp'),
+ -value => $data->{requiredForPasswordRecovery}
+ );
if ($data->{fieldType} eq "Image") {
$f->yesNo(
-name=>"forceImageOnly",
@@ -343,6 +349,7 @@ sub www_editProfileFieldSave {
visible=>$session->form->yesNo("visible"),
required=>$session->form->yesNo("required"),
showAtRegistration=>$session->form->yesNo("showAtRegistration"),
+ requiredForPasswordRecovery=>$session->form->yesNo("requiredForPasswordRecovery"),
possibleValues=>$session->form->textarea("possibleValues"),
dataDefault=>$session->form->textarea("dataDefault"),
fieldType=>$session->form->fieldType("fieldType"),
diff --git a/lib/WebGUI/ProfileField.pm b/lib/WebGUI/ProfileField.pm
index 0c35d3d77..60013a384 100644
--- a/lib/WebGUI/ProfileField.pm
+++ b/lib/WebGUI/ProfileField.pm
@@ -343,6 +343,19 @@ sub getRegistrationFields {
return $class->_listFieldsWhere($session, "f.showAtRegistration = 1");
}
+=head2 getPasswordRecoveryFields ( session )
+
+Returns an array reference of profile field objects that are required
+for password recovery. Class method.
+
+=cut
+
+sub getPasswordRecoveryFields {
+ my $class = shift;
+ my $session = shift;
+ return $class->_listFieldsWhere($session, "f.requiredForPasswordRecovery = 1");
+}
+
#-------------------------------------------------------------------
=head2 isEditable ( )
diff --git a/lib/WebGUI/i18n/English/AuthWebGUI.pm b/lib/WebGUI/i18n/English/AuthWebGUI.pm
index 170269016..d46d0c645 100644
--- a/lib/WebGUI/i18n/English/AuthWebGUI.pm
+++ b/lib/WebGUI/i18n/English/AuthWebGUI.pm
@@ -483,6 +483,21 @@ our $I18N = {
lastUpdated => 1128919828,
},
+ 'require username for password recovery' => {
+ message => q|Require Username for Password Recovery?|,
+ lastUpdated => 1165402566,
+ },
+
+ 'password recovery no results' => {
+ message => q|No users were found matching that profile data. Please try again.|,
+ lastUpdated => 1165402566,
+ },
+
+ 'password recovery multiple results' => {
+ message => q|Sorry, password recovery cannot be performed for this account. Please contact an administrator.|,
+ lastUpdated => 1165402566,
+ },
+
};
1;
diff --git a/lib/WebGUI/i18n/English/WebGUIProfile.pm b/lib/WebGUI/i18n/English/WebGUIProfile.pm
index 7b9fbe8a1..b88f02e48 100644
--- a/lib/WebGUI/i18n/English/WebGUIProfile.pm
+++ b/lib/WebGUI/i18n/English/WebGUIProfile.pm
@@ -351,6 +351,16 @@ new categories of profile settings.
message => "Show an entry for this field at the registration screen for newly-registering users. The field will not actually be required unless Required is also set.",
lastUpdated => 1164237018
},
+
+ 'requiredForPasswordRecovery label' => {
+ message => "Required for password recovery?",
+ lastUpdated => 1165401097
+ },
+
+ 'requiredForPasswordRecovery hoverHelp' => {
+ message => "Require users to enter this field for password recovery. Only users that enter all such fields correctly and uniquely to them will be able to perform password recovery.",
+ lastUpdated => 1165401097
+ },
};
1;