wiki.pl: Port some fixes from upstream
[Orgmuse.git] / modules / login.pl
blob6b4c6549e3606e8ef958490b192142288f933b1b
1 # Copyright (C) 2004 Fletcher T. Penney <fletcher@freeshell.org>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the
15 # Free Software Foundation, Inc.
16 # 59 Temple Place, Suite 330
17 # Boston, MA 02111-1307 USA
19 $ModulesDescription .= '<p><a href="http://git.savannah.gnu.org/cgit/oddmuse.git/tree/modules/login.pl">login.pl</a>, see <a href="http://www.oddmuse.org/cgi-bin/oddmuse/Login_Module">Login Module</a></p>';
21 #use vars qw($RegistrationForm $MinimumPasswordLength $RegistrationsMustBeApproved $LoginForm $PasswordFile $PendingPasswordFile $RequireLoginToEdit $ConfirmEmailAddress $ConfirmEmailAddress $UncomfirmedPasswordFile $EmailSenderAddress $EmailCommand $NotifyPendingRegistrations $EmailConfirmationMessage $ResetPasswordMessage $RegistrationForm $LogoutForm $ResetForm $ChangePassForm $RequireCamelUserName);
23 my $EncryptedPassword = "";
25 push(@MyAdminCode, \&LoginAdminRule);
27 $EmailRegExp = '[\w\.\-]+@([\w\-]+\.)+[\w]+';
28 $UsernameRegExp = '([A-Z][a-z]+){2,}';
29 $RequireCamelUserName = 0 unless defined $RequireCamelUserName;
31 $RequireLoginToEdit = 1 unless defined $RequireLoginToEdit;
32 $MinimumPasswordLength = 6 unless defined $MinimumPasswordLength;
33 $PasswordFile = "$DataDir/passwords" unless defined $PasswordFile;
35 $RegistrationsMustBeApproved = 1 unless defined $RegistrationsMustBeApproved;
36 $PendingPasswordFile = "$DataDir/pending" unless defined $PendingPasswordFile;
38 $ConfirmEmailAddress = 1 unless defined $ConfirmEmailAddress;
39 $UncomfirmedPasswordFile = "$DataDir/uncomfirmed" unless defined $UncomfirmedPasswordFile;
41 $EmailSenderAddress = "fletcher\@freeshell.org" unless defined $EmailSenderAddress;
42 $EmailCommand = "/usr/sbin/sendmail -oi -t" unless defined $EmailCommand;
44 $NotifyPendingRegistrations = "fletcher\@mercury.local" unless defined $NotifyPendingRegistrations;
46 $EmailConfirmationMessage = qq!From: $EmailSenderAddress
47 Subject: $SiteName Registration Confirmation
49 This email address was used to create an account at $SiteName. If you did not register at this site, you do not need to do anything.
51 Otherwise, in order to confirm your account, follow the link below.
53 Thank you...
55 ! unless defined $EmailConfirmationMessage;
57 $ResetPasswordMessage = qq!From: $EmailSenderAddress
58 Subject: $SiteName Password Reset
60 We received a request to reset your password on our website. Your password has been reset (see below). You may log in and change to a password of your choice.
62 Thank you...
64 ! unless defined $ResetPasswordMessage;
66 $PasswordFileToUse = $RegistrationsMustBeApproved
67 ? $PendingPasswordFile : $PasswordFile;
69 $PasswordFileToUse = $ConfirmEmailAddress
70 ? $UncomfirmedPasswordFile : $PasswordFileToUse;
72 $RegistrationForm = <<'EOT' unless defined $RegistrationForm;
73 <p>Your Username should be a CamelCase form of your real name, e.g. JohnDoe.</p>
75 <p>Your password must be at least 6 characters long.</p>
77 <p>Your email address must be real, as a confirmation email will be sent to you. Your email address will not be shared with anyone else, or used for any other purpose.</p>
79 <form method="post">
80 <input type="hidden" name="action" value="process_registration" />
81 <table class="form">
82 <tr><td class="label">
83 Username:
84 </td><td class="input">
85 <input type="text" name="username" value="%username%" />
86 </td></tr>
87 <tr><td class="label">
88 Password:
89 </td><td class="input">
90 <input type="password" name="pwd1" value="" />
91 </td></tr>
92 <tr><td class="label">
93 Reenter:
94 </td><td class="input">
95 <input type="password" name="pwd2" value="" />
96 </td></tr>
97 <tr><td class="label">
98 Email:
99 </td><td class="input">
100 <input type="text" name="email" value="%email%" />
101 </td></tr>
102 <tr><td colspan="2" class="button">
103 <input type="submit" value="Register" />
104 </td></tr>
105 </table>
106 </form>
109 $LoginForm = <<'EOT' unless defined $LoginForm;
110 <form method="post">
111 <input type="hidden" name="action" value="process_login" />
112 <table class="form">
113 <tr><td class="label">
114 Username:
115 </td><td class="input">
116 <input type="text" name="username" value="%username%" />
117 </td></tr>
118 <tr><td class="label">
119 Password:
120 </td><td class="input">
121 <input type="password" name="pwd" value="" />
122 </td></tr>
123 <tr><td colspan="2" class="button">
124 <input type="submit" value="Login" />
125 </td></tr>
126 </table>
127 </form>
130 $LogoutForm = <<'EOT' unless defined $LogoutForm;
131 <form method="post">
132 <input type="hidden" name="action" value="process_logout" />
133 <input type="hidden" name="pwd" value="" />
134 <table class="form">
135 <tr><td colspan="2" class="button">
136 <input type="submit" value="Logout" />
137 </td></tr>
138 </table>
139 </form>
142 $ResetForm = <<'EOT' unless defined $ResetForm;
143 <p>Submit your username in order to reset your password.</p>
144 <p>A temporary password will be mailed to you.</p>
145 <form method="post">
146 <input type="hidden" name="action" value="reset_password" />
147 <input type="hidden" name="pwd" value="" />
148 <table class="form">
149 <tr><td class="label">
150 Username:
151 </td><td class="input">
152 <input type="text" name="username" value="%username%" />
153 </td></tr>
154 <tr><td colspan="2" class="button">
155 <input type="submit" value="Reset" />
156 </td></tr>
157 </table>
158 </form>
161 $ChangePassForm = <<'EOT' unless defined $ChangePassForm;
162 <form method="post">
163 <input type="hidden" name="action" value="change_password" />
164 <table class="form">
165 <tr><td class="label">
166 Username:
167 </td><td class="input">
168 <input type="text" name="username" value="%username%" />
169 </td></tr>
170 <tr><td class="label">
171 Old Password:
172 </td><td class="input">
173 <input type="password" name="oldpwd" value="" />
174 </td></tr>
175 <tr><td class="label">
176 Password:
177 </td><td class="input">
178 <input type="password" name="pwd1" value="" />
179 </td></tr>
180 <tr><td class="label">
181 Reenter:
182 </td><td class="input">
183 <input type="password" name="pwd2" value="" />
184 </td></tr>
185 <tr><td colspan="2" class="button">
186 <input type="submit" value="Submit" />
187 </td></tr>
188 </table>
189 </form>
192 $Action{register} = \&DoRegister;
194 sub DoRegister {
195 my $id = shift;
196 print GetHeader('', Ts('Register for %s', $SiteName), '');
197 print '<div class="content">';
198 $RegistrationForm =~ s/\%([a-z]+)\%/GetParam($1)/ige;
199 $RegistrationForm =~ s/\$([a-z]+)\$/$q->span({-class=>'param'}, GetParam($1))
200 . $q->input({-type=>'hidden', -name=>$1, -value=>GetParam($1)})/ge;
201 print $RegistrationForm;
202 print '</div>';
203 PrintFooter();
207 $Action{process_registration} = \&DoProcessRegistration;
209 sub DoProcessRegistration {
210 my $id = shift;
211 my $username = GetParam('username', '');
212 my $pwd1 = GetParam('pwd1', '');
213 my $pwd2 = GetParam('pwd2', '');
214 my $email = GetParam('email', '');
216 if ($RequireCamelUserName) {
217 ReportError(T('Please choose a username of the form "FirstLast" using your real name.'))
218 unless ($username =~ /$UsernameRegExp/);
220 ReportError(T('The passwords do not match.'))
221 unless ($pwd1 eq $pwd2);
222 ReportError(Ts('The password must be at least %s characters.', $MinimumPasswordLength))
223 unless (length($pwd1) > ($MinimumPasswordLength-1));
224 ReportError(T('That email address is invalid.'))
225 unless ($email =~ /$EmailRegExp/);
226 ReportError(Ts('The username %s has already been registered.',$username))
227 if (UserExists($username));
229 print GetHeader('', Ts('Register for %s', $SiteName), '');
231 if ($RegistrationsMustBeApproved) {
232 if (AddUser($username,$pwd1,$email,$PasswordFileToUse)) {
233 print Ts('Your registration for %s has been submitted.', $SiteName);
234 print " ";
235 print T('Please allow time for the webmaster to approve your request.');
236 print " ";
237 if ($ConfirmEmailAddress) {
238 print Ts('An email has been sent to "%s" with further instructions.', $email);
239 print " ";
240 } else {
241 SendNotification($username);
243 } else {
244 ReportError(T('There was an error saving your registration.'));
246 } else {
247 if (AddUser($username, $pwd1, $email,$PasswordFileToUse)) {
248 print Ts('An account was created for %s.',$username);
249 print " ";
250 if ($ConfirmEmailAddress) {
251 print Ts('An email has been sent to "%s" with further instructions.', $email);
252 print " ";
254 } else {
255 ReportError(T('There was an error saving your registration.'));
259 SendConfirmationEmail($username,$email) if ($ConfirmEmailAddress);
261 PrintFooter();
264 $Action{login} = \&DoLogin;
266 sub DoLogin {
267 my $id = shift;
268 print GetHeader('', Ts('Login to %s', $SiteName), '');
269 print '<div class="content">';
270 $LoginForm =~ s/\%([a-z]+)\%/GetParam($1)/ge;
271 $LoginForm =~ s/\$([a-z]+)\$/$q->span({-class=>'param'}, GetParam($1))
272 . $q->input({-type=>'hidden', -name=>$1, -value=>GetParam($1)})/ge;
273 print $LoginForm;
274 print '</div>';
275 PrintFooter();
278 $Action{process_login} = \&DoProcessLogin;
280 sub DoProcessLogin {
281 my $id = shift;
282 my $username = GetParam('username', '');
283 my $pwd = GetParam('pwd', '');
284 my $email = GetParam('email', '');
286 ReportError(T('Username and/or password are incorrect.'))
287 unless (AuthenticateUser($username,$pwd));
289 unlink($IndexFile);
290 print GetHeader('', Ts('Register for %s', $SiteName), '');
291 print '<div class="content">';
292 print Ts('Logged in as %s.', $username);
293 print '</div>';
294 PrintFooter();
297 $Action{logout} = \&DoLogout;
299 sub DoLogout {
300 my $id = shift;
301 print GetHeader('', Ts('Logout of %s', $SiteName), '');
302 print '<div class="content">';
303 print '<p>' . Ts('Logout of %s?',$SiteName) . '</p>';
304 $LogoutForm =~ s/\%([a-z]+)\%/GetParam($1)/ge;
305 $LogoutForm =~ s/\$([a-z]+)\$/$q->span({-class=>'param'}, GetParam($1))
306 . $q->input({-type=>'hidden', -name=>$1, -value=>GetParam($1)})/ge;
307 print $LogoutForm;
308 print '</div>';
309 PrintFooter();
312 $Action{process_logout} = \&DoProcessLogout;
314 sub DoProcessLogout {
315 SetParam('pwd','');
316 SetParam('username','');
317 unlink($IndexFile); # I shouldn't have to do this...
318 print GetHeader('', Ts('Logged out of %s', $SiteName), '');
319 print '<div class="content">';
320 print T('You are now logged out.');
321 print '</div>';
322 PrintFooter();
325 sub UserExists {
326 my $username = shift;
327 if (open (PASSWD, $PasswordFile)) {
328 while ( <PASSWD> ) {
329 if ($_ =~ /^$username:/) {
330 return 1;
334 close PASSWD;
336 if ($RegistrationsMustBeApproved) {
337 if (open (PASSWD, $PendingPasswordFile)) {
338 while ( <PASSWD> ) {
339 if ($_ =~ /^$username:/) {
340 return 1;
344 close PASSWD;
347 if ($ConfirmEmailAddress) {
348 if (open (PASSWD, $UncomfirmedPasswordFile)) {
349 while ( <PASSWD> ) {
350 if ($_ =~ /^$username:/) {
351 return 1;
355 close PASSWD;
358 return 0;
361 sub AddUser {
362 my ($username, $pwd, $email, $FileToUse) = @_;
364 my @salts = (a..z,A..Z,0..9,'.','/');
365 my $salt=$salts[rand @salts];
366 $salt.=$salts[rand @salts];
367 my $encrypted = crypt($pwd,$salt);
368 $EncryptedPassword = $encrypted;
370 my %passwords = ();
371 my %emails = ();
372 my $key;
374 if (open (PASSWD, $FileToUse)) {
375 while ( <PASSWD> ) {
376 if ($_ =~ /^(.*):(.*):(.*)$/) {
377 $passwords{$1}=$2;
378 $emails{$1}=$3;
382 close PASSWD;
384 $passwords{$username} = $encrypted;
385 $emails{$username} = $email;
387 open (PASSWD, ">$FileToUse");
388 foreach $key ( sort keys(%passwords)) {
389 print PASSWD "$key:$passwords{$key}:$emails{$key}\n";
391 close PASSWD;
393 return 1;
397 *OldUserCanEdit = *UserCanEdit;
398 *UserCanEdit = *LoginUserCanEdit;
400 sub LoginUserCanEdit {
401 my ($id, $editing) = @_;
403 my $user = GetParam('username', '');
404 my $pwd = GetParam('pwd', '');
406 if ($RequireLoginToEdit) {
407 if ($user and $pwd) {
408 # If not logged in, return 0. Otherwise, let Oddmuse d$
409 return 0 unless AuthenticateUser($user, $pwd);
410 return OldUserCanEdit($id, $editing);
412 return 0;
414 return OldUserCanEdit($id, $editing);
417 sub AuthenticateUser {
418 my ($username, $password) = @_;
419 my $line;
421 if (open(PASSWD, $PasswordFile)) {
422 while ($line = <PASSWD>) {
423 if ($line =~ /^$username:(.*):(.*)/) {
424 if (crypt($password,$1) eq $1) {
425 close PASSWD;
426 return 1;
431 close PASSWD;
432 return 0;
435 sub LoginAdminRule {
436 ($id, $menuref, *restref) = @_;
438 push(@$menuref, ScriptLink('action=register', T('Register a new account'), 'register'));
439 push(@$menuref, ScriptLink('action=login', T('Login'), 'login'));
440 push(@$menuref, ScriptLink('action=logout', T('Logout'), 'logout'));
441 push(@$menuref, ScriptLink('action=whoami', T('Who am I?'), 'whoami'));
442 push(@$menuref, ScriptLink('action=reset', T('Forgot your password?'), 'reset'));
443 push(@$menuref, ScriptLink('action=change', T('Change your password'), 'change'));
445 if (UserIsAdmin()) {
446 push(@$menuref, ScriptLink('action=approve_pending', T('Approve pending registrations'), 'approve'));
450 sub SendConfirmationEmail {
451 my ($username, $email) = @_;
452 my $key = $EncryptedPassword;
453 my @salts = (a..z,A..Z,0..9,'.','/');
454 my $salt=$salts[rand @salts];
455 $salt.=$salts[rand @salts];
456 my $encrypted = crypt($key,$salt);
458 $confirmationLink = "$FullUrl?action=confirm_registration;account=$username;key=$encrypted;";
460 open (MAIL, "| $EmailCommand");
461 print MAIL "To: $email\n$EmailConfirmationMessage\n\nClick on the following link to confirm:\n\n$confirmationLink\n\n";
462 close MAIL;
466 $Action{confirm_registration} = \&DoConfirmRegistration;
468 sub DoConfirmRegistration {
469 my $id = shift;
470 my $account = GetParam('account', '');
471 my $key = GetParam('key', '');
473 if ( ConfirmUser($account,$key)) {
474 print GetHeader('', Ts('Confirm Registration for %s', $SiteName), '');
476 print Ts('%s, your registration has been approved. You can now use your password to login and edit this wiki.',$account);
478 PrintFooter();
480 } else {
481 ReportError(Ts('Confirmation failed. Please email %s for help.', $EmailSenderAddress));
486 sub ConfirmUser {
487 my ($username, $key) = @_;
488 my $FileToUse = $RegistrationsMustBeApproved
489 ? $PendingPasswordFile : $PasswordFileToUse;
491 if (open(PASSWD, $UncomfirmedPasswordFile)) {
492 while (<PASSWD>) {
493 if ($_ =~ /^$username:(.*):(.*)/) {
494 if (crypt($1,$key) eq $key) {
495 AddUser($username,$1,$2,$FileToUse);
496 close PASSWD;
497 RemoveUser($username,$UncomfirmedPasswordFile);
498 if ($RegistrationsMustBeApproved) {
499 SendNotification($username);
501 return 1;
506 return 0;
510 sub RemoveUser {
511 my ($username, $FileToUse) = @_;
513 my %passwords = ();
514 my %emails = ();
515 my $key;
517 if (open (PASSWD, $FileToUse)) {
518 while ( <PASSWD> ) {
519 if ($_ =~ /^(.*):(.*):(.*)$/) {
520 next if ($1 eq $username);
521 $passwords{$1}=$2;
522 $emails{$1}=$3;
526 close PASSWD;
528 open (PASSWD, ">$FileToUse");
529 foreach $key ( sort keys(%passwords)) {
530 print PASSWD "$key:$passwords{$key}:$emails{$key}\n";
532 close PASSWD;
534 return 1;
537 $Action{whoami} = \&DoWhoAmI;
539 sub DoWhoAmI {
540 print GetHeader('', T('Who Am I?'), '');
541 my $user = GetParam('username', '');
542 my $pwd = GetParam('pwd', '');
544 if (AuthenticateUser($user, $pwd)) {
545 print Ts('You are logged in as %s.',GetParam('username', ''));
546 } else {
547 print T('You are not logged in.');
549 PrintFooter();
553 $Action{reset_password} = \&DoResetPassword;
555 sub DoResetPassword {
556 my $id = shift;
557 my $username = GetParam('username', '');
559 if (UserExists($username)) {
560 my ($newpass, $newhash) = newpass();
562 my $email = ChangePassword($username,$newhash);
564 if ($email ne "") {
565 print GetHeader('', T('Reset Password'), '');
566 print Ts('The password for %s was reset. It has been emailed to the address on file.',$username);
567 PrintFooter();
568 SendResetEmail($email,$newpass);
569 } else {
570 ReportError(Ts('There was an error resetting the password for %s.',$username));
572 } else {
573 ReportError(Ts('The username "%s" does not exist.',$username));
577 sub newpass {
578 # Create a random password
580 my @salts = (a..z,A..Z,0..9,'.','/');
581 my $salt=$salts[rand @salts];
582 $salt.=$salts[rand @salts];
584 my $password = $salts[rand @salts];
586 for ( $i=0; $i < 7; $i++) {
587 $password .= $salts[rand @salts];
590 my $hash = crypt($password, $salt);
592 return ($password, $hash);
595 sub ChangePassword {
596 my ($user, $hash) = @_;
598 my %passwords = ();
599 my %emails = ();
600 my $key;
602 if (open (PASSWD, $PasswordFile)) {
603 while ( <PASSWD> ) {
604 if ($_ =~ /^(.*):(.*):(.*)$/) {
605 $passwords{$1}=$2;
606 $emails{$1}=$3;
610 close PASSWD;
612 $passwords{$user} = $hash;
614 open (PASSWD, ">$PasswordFile");
615 foreach $key ( sort keys(%passwords)) {
616 print PASSWD "$key:$passwords{$key}:$emails{$key}\n";
618 close PASSWD;
620 return $emails{$user};
623 $Action{reset} = \&DoReset;
625 sub DoReset {
626 my $id = shift;
627 print GetHeader('', Ts('Reset Password for %s', $SiteName), '');
628 print '<div class="content">';
629 print '<p>' . T('Reset Password?') . '</p>';
630 $ResetForm =~ s/\%([a-z]+)\%/GetParam($1)/ge;
631 $ResetForm =~ s/\$([a-z]+)\$/$q->span({-class=>'param'}, GetParam($1))
632 . $q->input({-type=>'hidden', -name=>$1, -value=>GetParam($1)})/ge;
633 print $ResetForm;
634 print '</div>';
635 PrintFooter();
638 sub SendResetEmail {
639 my ($email, $newpass) = @_;
641 open (MAIL, "| $EmailCommand");
642 print MAIL "To: $email\n$EmailConfirmationMessage\n\nYour new temporary password:\n\n$newpass\n\n";
643 close MAIL;
648 $Action{change} = \&DoChangePassword;
650 sub DoChangePassword {
651 my $id = shift;
652 print GetHeader('', Ts('Change Password for %s', $SiteName), '');
653 print '<div class="content">';
654 print '<p>' . T('Change Password?') . '</p>';
655 $ChangePassForm =~ s/\%([a-z]+)\%/GetParam($1)/ge;
656 $ChangePassForm =~ s/\$([a-z]+)\$/$q->span({-class=>'param'}, GetParam($1))
657 . $q->input({-type=>'hidden', -name=>$1, -value=>GetParam($1)})/ge;
658 print $ChangePassForm;
659 print '</div>';
660 PrintFooter();
663 $Action{change_password} = \&DoProcessChangePassword;
665 sub DoProcessChangePassword {
666 my $id = shift;
667 my $username = GetParam('username', '');
668 my $pwd1 = GetParam('pwd1', '');
669 my $pwd2 = GetParam('pwd2', '');
670 my $oldpwd = GetParam('oldpwd', '');
672 ReportError(T('Your current password is incorrect.')) if
673 (! AuthenticateUser($username,$oldpwd));
675 ReportError(T('The passwords do not match.'))
676 unless ($pwd1 eq $pwd2);
677 ReportError(Ts('The password must be at least %s characters.', $MinimumPasswordLength))
678 unless (length($pwd1) > ($MinimumPasswordLength-1));
680 print GetHeader('', Ts('Register for %s', $SiteName), '');
682 my @salts = (a..z,A..Z,0..9,'.','/');
683 my $salt=$salts[rand @salts];
684 $salt.=$salts[rand @salts];
685 my $encrypted = crypt($pwd1,$salt);
687 ChangePassword($username,$encrypted);
689 print T('Your password has been changed.');
690 PrintFooter();
693 sub SendNotification {
694 my $NewUser = shift;
696 open (MAIL, "| $EmailCommand");
697 print MAIL "To: $NotifyPendingRegistrations\nFrom: $EmailSenderAddress\nSubject: New User at $SiteName\n\nYou have a new pending registration at $SiteName:\n\n$NewUser\n\n";
698 close MAIL;
702 $Action{approve_pending} = \&DoApprovePending;
704 sub DoApprovePending {
705 my $id = shift;
706 my $count = 0;
708 my $ToBeApproved = GetParam('user','');
710 UserIsAdminOrError();
712 print GetHeader('', Ts('Approve Pending Registrations for %s', $SiteName), '');
714 if ($ToBeApproved) {
715 if (ApproveUser($ToBeApproved)) {
716 print Ts('%s has been approved.',$ToBeApproved);
717 } else {
718 print Ts('There was an error approving %s.',$ToBeApproved);
720 } else {
721 print T('<ul>');
722 if (open(PASSWD, $PendingPasswordFile)) {
723 while (<PASSWD>) {
724 if ($_ =~ /^(.*):(.*):(.*)$/) {
725 print Tss('<li>%1 - %2</li>',ScriptLink("action=approve_pending;user=$1;",$1),"$3");
726 $count++;
730 print T('</ul>');
732 if ($count == 0) {
733 print T('There are no pending registrations.');
737 PrintFooter();
741 sub ApproveUser {
742 my ($username) = @_;
744 if (open(PASSWD, $PendingPasswordFile)) {
745 while (<PASSWD>) {
746 if ($_ =~ /^$username:(.*):(.*)/) {
747 AddUser($username,$1,$2,$PasswordFile);
748 close PASSWD;
749 RemoveUser($username,$PendingPasswordFile);
750 return 1;
754 return 0;