bugfixes
[ikiwiki] / IkiWiki / Plugin / passwordauth.pm
1 #!/usr/bin/perl
2 # Ikiwiki password authentication.
3 package IkiWiki::Plugin::passwordauth;
4
5 use warnings;
6 use strict;
7 use IkiWiki 2.00;
8
9 sub import { #{{{
10         hook(type => "formbuilder_setup", id => "passwordauth",
11                 call => \&formbuilder_setup);
12         hook(type => "formbuilder", id => "passwordauth",
13                 call => \&formbuilder);
14         hook(type => "sessioncgi", id => "passwordauth", call => \&sessioncgi);
15 } # }}}
16
17 # Checks if a string matches a user's password, and returns true or false.
18 sub checkpassword ($$;$) { #{{{
19         my $user=shift;
20         my $password=shift;
21         my $field=shift || "password";
22
23         # It's very important that the user not be allowed to log in with
24         # an empty password!
25         if (! length $password) {
26                 return 0;
27         }
28
29         my $userinfo=IkiWiki::userinfo_retrieve();
30         if (! length $user || ! defined $userinfo ||
31             ! exists $userinfo->{$user} || ! ref $userinfo->{$user}) {
32                 return 0;
33         }
34
35         my $ret=0;
36         if (exists $userinfo->{$user}->{"crypt".$field}) {
37                 eval q{use Authen::Passphrase};
38                 error $@ if $@;
39                 my $p = Authen::Passphrase->from_crypt($userinfo->{$user}->{"crypt".$field});
40                 $ret=$p->match($password);
41         }
42         elsif (exists $userinfo->{$user}->{$field}) {
43                 $ret=$password eq $userinfo->{$user}->{$field};
44         }
45
46         if ($ret &&
47             (exists $userinfo->{$user}->{resettoken} ||
48              exists $userinfo->{$user}->{cryptresettoken})) {
49                 # Clear reset token since the user has successfully logged in.
50                 delete $userinfo->{$user}->{resettoken};
51                 delete $userinfo->{$user}->{cryptresettoken};
52                 IkiWiki::userinfo_store($userinfo);
53         }
54
55         return $ret;
56 } #}}}
57
58 sub setpassword ($$;$) { #{{{
59         my $user=shift;
60         my $password=shift;
61         my $field=shift || "password";
62
63         eval q{use Authen::Passphrase::BlowfishCrypt};
64         if (! $@) {
65                 my $p = Authen::Passphrase::BlowfishCrypt->new(
66                         cost => $config{password_cost} || 8,
67                         salt_random => 1,
68                         passphrase => $password,
69                 );
70                 IkiWiki::userinfo_set($user, "crypt$field", $p->as_crypt);
71                 IkiWiki::userinfo_set($user, $field, "");
72         }
73         else {
74                 IkiWiki::userinfo_set($user, $field, $password);
75         }
76 } #}}}
77
78 sub formbuilder_setup (@) { #{{{
79         my %params=@_;
80
81         my $form=$params{form};
82         my $session=$params{session};
83         my $cgi=$params{cgi};
84
85         if ($form->title eq "signin" || $form->title eq "register") {
86                 $form->field(name => "name", required => 0);
87                 $form->field(name => "password", type => "password", required => 0);
88                 
89                 if ($form->submitted eq "Register" || $form->submitted eq "Create Account") {
90                         $form->field(name => "confirm_password", type => "password");
91                         $form->field(name => "account_creation_password", type => "password") if (length $config{account_creation_password});
92                         $form->field(name => "email", size => 50);
93                         $form->title("register");
94                         $form->text("");
95                 
96                         $form->field(name => "confirm_password",
97                                 validate => sub {
98                                         shift eq $form->field("password");
99                                 },
100                         );
101                         $form->field(name => "password",
102                                 validate => sub {
103                                         shift eq $form->field("confirm_password");
104                                 },
105                         );
106                 }
107
108                 if ($form->submitted) {
109                         my $submittype=$form->submitted;
110                         # Set required fields based on how form was submitted.
111                         my %required=(
112                                 "Login" => [qw(name password)],
113                                 "Register" => [],
114                                 "Create Account" => [qw(name password confirm_password email)],
115                                 "Reset Password" => [qw(name)],
116                         );
117                         foreach my $opt (@{$required{$submittype}}) {
118                                 $form->field(name => $opt, required => 1);
119                         }
120         
121                         if ($submittype eq "Create Account") {
122                                 $form->field(
123                                         name => "account_creation_password",
124                                         validate => sub {
125                                                 shift eq $config{account_creation_password};
126                                         },
127                                         required => 1,
128                                 ) if (length $config{account_creation_password});
129                                 $form->field(
130                                         name => "email",
131                                         validate => "EMAIL",
132                                 );
133                         }
134
135                         # Validate password against name for Login.
136                         if ($submittype eq "Login") {
137                                 $form->field(
138                                         name => "password",
139                                         validate => sub {
140                                                 checkpassword($form->field("name"), shift);
141                                         },
142                                 );
143                         }
144                         elsif ($submittype eq "Register" ||
145                                $submittype eq "Create Account" ||
146                                $submittype eq "Reset Password") {
147                                 $form->field(name => "password", validate => 'VALUE');
148                         }
149                         
150                         # And make sure the entered name exists when logging
151                         # in or sending email, and does not when registering.
152                         if ($submittype eq 'Create Account' ||
153                             $submittype eq 'Register') {
154                                 $form->field(
155                                         name => "name",
156                                         validate => sub {
157                                                 my $name=shift;
158                                                 length $name &&
159                                                 $name=~/$config{wiki_file_regexp}/ &&
160                                                 ! IkiWiki::userinfo_get($name, "regdate");
161                                         },
162                                 );
163                         }
164                         elsif ($submittype eq "Login" ||
165                                $submittype eq "Reset Password") {
166                                 $form->field( 
167                                         name => "name",
168                                         validate => sub {
169                                                 my $name=shift;
170                                                 length $name &&
171                                                 IkiWiki::userinfo_get($name, "regdate");
172                                         },
173                                 );
174                         }
175                 }
176                 else {
177                         # First time settings.
178                         $form->field(name => "name");
179                         if ($session->param("name")) {
180                                 $form->field(name => "name", value => $session->param("name"));
181                         }
182                 }
183         }
184         elsif ($form->title eq "preferences") {
185                 $form->field(name => "name", disabled => 1, 
186                         value => $session->param("name"), force => 1,
187                         fieldset => "login");
188                 $form->field(name => "password", type => "password",
189                         fieldset => "login",
190                         validate => sub {
191                                 shift eq $form->field("confirm_password");
192                         }),
193                 $form->field(name => "confirm_password", type => "password",
194                         fieldset => "login",
195                         validate => sub {
196                                 shift eq $form->field("password");
197                         }),
198         }
199 }
200
201 sub formbuilder (@) { #{{{
202         my %params=@_;
203
204         my $form=$params{form};
205         my $session=$params{session};
206         my $cgi=$params{cgi};
207         my $buttons=$params{buttons};
208
209         if ($form->title eq "signin" || $form->title eq "register") {
210                 if ($form->submitted && $form->validate) {
211                         if ($form->submitted eq 'Login') {
212                                 $session->param("name", $form->field("name"));
213                                 IkiWiki::cgi_postsignin($cgi, $session);
214                         }
215                         elsif ($form->submitted eq 'Create Account') {
216                                 my $user_name=$form->field('name');
217                                 if (IkiWiki::userinfo_setall($user_name, {
218                                         'email' => $form->field('email'),
219                                         'regdate' => time})) {
220                                         setpassword($user_name, $form->field('password'));
221                                         $form->field(name => "confirm_password", type => "hidden");
222                                         $form->field(name => "email", type => "hidden");
223                                         $form->text(gettext("Account creation successful. Now you can Login."));
224                                 }
225                                 else {
226                                         error(gettext("Error creating account."));
227                                 }
228                         }
229                         elsif ($form->submitted eq 'Reset Password') {
230                                 my $user_name=$form->field("name");
231                                 my $email=IkiWiki::userinfo_get($user_name, "email");
232                                 if (! length $email) {
233                                         error(gettext("No email address, so cannot email password reset instructions."));
234                                 }
235                                 
236                                 # Store a token that can be used once
237                                 # to log the user in. This needs to be hard
238                                 # to guess. Generating a cgi session id will
239                                 # make it as hard to guess as any cgi session.
240                                 eval q{use CGI::Session};
241                                 error($@) if $@;
242                                 my $token = CGI::Session->new->id;
243                                 setpassword($user_name, $token, "resettoken");
244                                 
245                                 my $template=template("passwordmail.tmpl");
246                                 $template->param(
247                                         user_name => $user_name,
248                                         passwordurl => IkiWiki::cgiurl(
249                                                 'do' => "reset",
250                                                 'name' => $user_name,
251                                                 'token' => $token,
252                                         ),
253                                         wikiurl => $config{url},
254                                         wikiname => $config{wikiname},
255                                         REMOTE_ADDR => $ENV{REMOTE_ADDR},
256                                 );
257                                 
258                                 eval q{use Mail::Sendmail};
259                                 error($@) if $@;
260                                 sendmail(
261                                         To => IkiWiki::userinfo_get($user_name, "email"),
262                                         From => "$config{wikiname} admin <$config{adminemail}>",
263                                         Subject => "$config{wikiname} information",
264                                         Message => $template->output,
265                                 ) or error(gettext("Failed to send mail"));
266                                 
267                                 $form->text(gettext("You have been mailed password reset instructions."));
268                                 $form->field(name => "name", required => 0);
269                                 push @$buttons, "Reset Password";
270                         }
271                         elsif ($form->submitted eq "Register") {
272                                 @$buttons="Create Account";
273                         }
274                 }
275                 elsif ($form->submitted eq "Create Account") {
276                         @$buttons="Create Account";
277                 }
278                 else {
279                         push @$buttons, "Register", "Reset Password";
280                 }
281         }
282         elsif ($form->title eq "preferences") {
283                 if ($form->submitted eq "Save Preferences" && $form->validate) {
284                         my $user_name=$form->field('name');
285                         if ($form->field("password") && length $form->field("password")) {
286                                 setpassword($user_name, $form->field('password'));
287                         }
288                 }
289         }
290 } #}}}
291
292 sub sessioncgi ($$) { #{{{
293         my $q=shift;
294         my $session=shift;
295
296         if ($q->param('do') eq 'reset') {
297                 my $name=$q->param("name");
298                 my $token=$q->param("token");
299
300                 if (! defined $name || ! defined $token ||
301                     ! length $name  || ! length $token) {
302                         error(gettext("incorrect password reset url"));
303                 }
304                 if (! checkpassword($name, $token, "resettoken")) {
305                         error(gettext("password reset denied"));
306                 }
307
308                 $session->param("name", $name);
309                 IkiWiki::cgi_prefs($q, $session);
310                 exit;
311         }
312 } #}}}
313
314 1