]> mj.ucw.cz Git - pciids.git/blob - PciIds/Html/Users.pm
Parent is notified about new item
[pciids.git] / PciIds / Html / Users.pm
1 package PciIds::Html::Users;
2 use strict;
3 use warnings;
4 use PciIds::Html::Util;
5 use PciIds::Html::Forms;
6 use PciIds::Email;
7 use PciIds::Users;
8 use PciIds::Address;
9 use CGI;
10 use CGI::Cookie;
11 use Apache2::Const qw(:common);
12 use Apache2::SubRequest;
13 use APR::Table;
14
15 use base 'Exporter';
16
17 our @EXPORT = qw(&checkLogin &notLoggedComplaint);
18
19 sub genRegisterForm( $$$$ ) {
20         my( $req, $args, $error, $values ) = @_;
21         genHtmlHead( $req, 'Register a new user', undef );
22         genCustomHead( $req, $args, PciIds::Address::new( $req->uri() ), 'Register a new user', [ [ 'Help', 'help', 'account' ] ], [ [ 'Log in', 'login' ] ] );
23         print '<div class="error">'.$error.'</div>' if( defined $error );
24         print '<form name="register" id="register" method="POST" action="">
25                 <table>';
26         genForm( [ [ 'Email:', 'text', 'email', 'maxlength="255"' ],
27                 [ '', 'submit', 'register', 'value="Register"' ] ], $values );
28         print '</table></form>';
29         genHtmlTail();
30         return OK;
31 }
32
33 sub registerForm( $$ ) {#Form for registering a new user
34         my( $req, $args ) = @_;
35         return genRegisterForm( $req, $args, undef, {} );
36 }
37
38 sub loginCheck( $$ ) {
39         my( $login, $tables ) = @_;
40         return undef if( ( not defined $login ) || ( $login eq '' ) );#empty login is ok
41         return 'Login too long' if( ( length $login ) > 50 );
42         return 'Login contains invalid characters' unless( $login =~ /^[-_a-zA-Z0-9]+$/ );
43         return 'This login already exists' if( $tables->hasLogin( $login ) );
44         return undef;
45 }
46
47 sub registerSubmit( $$$ ) {#A registration form has been submited
48         my( $req, $args, $tables ) = @_;
49         my( $data, $error ) = getForm( {
50                 'email' => sub {
51                         return emailCheck( shift, $tables );
52                 }
53         }, [] );
54         return genRegisterForm( $req, $args, $error, $data ) if( defined $error );
55         my $site = $req->hostname();
56         my $url = 'https://'.$req->hostname().setAddrPrefix( $req->uri(), 'mods' );
57         sendMail( $data->{'email'}, 'Confirm registration', "Someone, probably you, requested registration of this address\n".
58                 "for the $site site. If it wasn't you, please ignore this email message.\n".
59                 "\nOtherwise, please continue by filling in the form at this address:".
60                 "\n".$url.'?action=register-confirm?email='.$data->{'email'}.'?confirm='.emailConfirm( $data->{'email'} )."\n".
61                 "\nThank you\n".
62                 "\n(This is an autogenerated email, do not respond to it)" );
63         genHtmlHead( $req, 'Registration email sent', undef );
64         genCustomHead( $req, $args, PciIds::Address::new( $req->uri() ), 'Registration email sent', [ [ 'Help', 'help', 'account' ] ], [ [ 'Log in', 'login' ] ] );
65         print "<div class='clear'></div></div>\n";
66         print '<p>
67                         An email containing further information has been sent to you.
68                         Please follow these instruction to finish the registration process.';
69         genHtmlTail();
70         return OK;
71 }
72
73 sub genConfirmForm( $$$$ ) {
74         my( $req, $args, $error, $values ) = @_;
75         genHtmlHead( $req, 'Confirm registration', undef );
76         genCustomHead( $req, $args, PciIds::Address::new( $req->uri() ), 'Confirm registration', [ [ 'Help', 'help', 'account' ] ], [ [ 'Register', 'register' ] ] );
77         print '<div class="error">'.$error.'</div>' if( defined $error );
78         print '<p>Email address: '.encode( $values->{'email'} );
79         print '<form name="register-confirm" id="register-confirm" method="POST" action="">';
80         print '<div class="hidden"><p><input type="hidden" value="'.encode( $values->{'email'} ).'" name="email"><input type="hidden" value="'.encode( $values->{'confirm'} ).'" name="confirm"></div>';
81         print '<table>';
82         genForm( [ [ 'Login (Optional):', 'text', 'login', 'maxlength="50"' ],
83                 [ 'Password:', 'password', 'password' ],
84                 [ 'Confirm password:', 'password', 'confirm_password' ],
85                 [ '', 'submit', 'register', 'value=Register' ] ], $values );
86         print '</table></form>';
87         genHtmlTail();
88         return OK;
89 }
90
91 sub usedAddress( $$ ) {
92         my( $req, $args ) = @_;
93         genHtmlHead( $req, 'Used address', undef );
94         genCustomHead( $req, $args, PciIds::Address::new( $req->uri() ), 'Used address', [ [ 'Help', 'help', 'account' ] ], [ [ 'Log in', 'login' ], [ 'Reset password', 'respass' ], [ 'Register', 'register' ] ] );
95         print '<div class="error">
96                 <p>
97                         An account for this address is already registered.
98                         Please, reset or remember your password or start again with a different address.
99                 </div>';
100         genHtmlTail();
101         return 0;
102 }
103
104 sub checkRegHash( $$$$$ ) {
105         my( $req, $args, $tables, $email, $hash ) = @_;
106         if( ! checkConfirmHash( $email, $hash ) ) {
107                 genHtmlHead( $req, 'Invalid registration request', undef );
108                 print '<h1>Invalid registration request</h1>
109                         <div class="error">
110                         <p>
111                                 This registration request is invalid.
112                                 Are you sure you got it from the registration email?
113                         </div>';
114                 genHtmlTail();
115                 return 0;
116         } elsif( $tables->hasEmail( $email ) ) {
117                 return usedAddress( $req, $args );
118         } else {
119                 return 1;
120         }
121 }
122
123 sub confirmForm( $$$$ ) {
124         my( $req, $args, $tables, $auth ) = @_;
125         return HTTPRedirect( $req, 'https://'.$req->hostname().$req->uri().buildArgs( $args ) ) unless $auth->{'ssl'};
126         if( ! checkRegHash( $req, $args, $tables, $args->{'email'}, $args->{'confirm'} ) ) {
127                 return OK;
128         } else {
129                 return genConfirmForm( $req, $args, undef, $args );
130         }
131 }
132
133 sub passLenCheck( $ ) {
134         my( $pass ) = @_;
135         return ( ( length $pass ) >= 4 ) ? undef : 'Password must have at least 4 characters';
136 }
137
138 sub passSameCheck( $ ) {
139         my( $data ) = @_;
140         return ( ( ( defined $data->{'password'} ) != ( defined $data->{'confirm_password'} ) ) || ( ( defined $data->{'password'} ) && ( $data->{'password'} ne $data->{'confirm_password'} ) ) ) ? 'Passwords do not match' : undef;
141 }
142
143 sub confirmSubmit( $$$ ) {
144         my( $req, $args, $tables ) = @_;
145         my( $data, $error ) = getForm( {
146                 'email' => sub {
147                         return emailCheck( shift, $tables );
148                 },
149                 'confirm' => undef,
150                 'login' => sub {
151                         return loginCheck( shift, $tables );
152                 },
153                 'password' => \&passLenCheck,
154                 'confirm_password' => undef }, [ \&passSameCheck ] );
155         return OK if( ! checkRegHash( $req, $args, $tables, $data->{'email'}, $data->{'confirm'} ) );#Not much info, but this is an attack anyway
156         return genConfirmForm( $req, $args, $error, $data ) if( defined $error );
157         unless( addUser( $tables, $data->{'login'}, $data->{'email'}, $data->{'password'} ) ) {
158                 usedAddress( $req, $args );
159                 return OK;
160         }
161         genHtmlHead( $req, 'Registered', undef );
162         genCustomHead( $req, $args, PciIds::Address::new( $req->uri() ), 'Registered', [ [ 'Help', 'help', 'account' ] ], [ [ 'Log in', 'login' ] ] );
163         print '<p>
164                         You have registered successfully.';
165         genHtmlTail();
166         return OK;
167 }
168
169 sub genLoginForm( $$$$ ) {
170         my( $req, $args, $error, $values ) = @_;
171         $req->headers_out->add( 'Set-Cookie' => new CGI::Cookie( -name => 'cookie-test', -value => 1 ) );
172         genHtmlHead( $req, 'Log in', undef );
173         genCustomHead( $req, $args, PciIds::Address::new( $req->uri() ), 'Log in', [ [ 'Help', 'help', 'account' ] ], [ [ 'Register', 'register' ], [ 'Reset password', 'respass' ] ] );
174         print '<div class="error"><p>'.$error.'</div>' if( defined $error );
175         print '<form name="login" id="login" method="POST" action="'.setAddrPrefix( $req->uri(), 'mods' ).buildExcept( 'action', $args ).'?action=login"><table>';
176         genForm( [ [ 'Login name or email:', 'text', 'login', 'maxlength="255"' ],
177                 [ 'Password:', 'password', 'password' ],
178                 [ '', 'submit', 'login', 'value="Login"' ] ], $values );
179         print '</table></form>';
180         genHtmlTail();
181         return OK;
182 }
183
184 sub loginForm( $$$ ) {
185         my( $req, $args, $tables, $auth ) = @_;
186         return HTTPRedirect( $req, 'https://'.$req->hostname().$req->uri().buildArgs( $args ) ) unless( $auth->{'ssl'} );
187         return genLoginForm( $req, $args, undef, {} );
188 }
189
190 sub loginSubmit( $$$ ) {
191         my( $req, $args, $tables ) = @_;
192         my( $data, $error ) = getForm( {
193                 'login' => undef,
194                 'password' => undef
195         }, [] );
196         my $logged = 0;
197         my $cookies = fetch CGI::Cookie;
198         unless( $cookies->{'cookie-test'} ) {
199                 return genLoginForm( $req, $args, 'You need to enable cookies', $data );
200         }
201         my( $id, $passwd, $email, $last ) = $tables->getLogInfo( $data->{'login'} );
202         if( defined $passwd && defined $data->{'password'} ) {
203                 my $salted = saltedPasswd( $email, $data->{'password'} );
204                 $logged = $salted eq $passwd;
205         }
206         if( $logged ) {
207                 $req->err_headers_out->add( 'Set-Cookie' => new CGI::Cookie( -name => 'auth', -value => genAuthToken( $tables, $id, $req, undef, $email ) ) );
208                 $args->{'action'} = ( defined $args->{'redirectaction'} && $args->{'redirectaction'} ne '' ) ? $args->{'redirectaction'} : 'list';
209                 my $url = 'http://'.$req->hostname().setAddrPrefix( $req->uri(), $args->{'action'} eq 'list' ? 'read' : 'mods' ).buildExcept( 'redirectaction', $args );
210                 return HTTPRedirect( $req, $url );
211         } else {
212                 return genLoginForm( $req, $args, 'Invalid login credetials', $data );
213         }
214 }
215
216 sub logout( $$ ) {
217         my( $req, $args ) = @_;
218         $req->err_headers_out->add( 'Set-Cookie' => new CGI::Cookie( -name => 'auth', -value => '0' ) );
219         return HTTPRedirect( $req, 'http://'.$req->hostname().setAddrPrefix( $req->uri(), 'read' ).buildExcept( 'action', $args ) );
220 }
221
222 sub checkLogin( $$ ) {
223         my( $req, $tables ) = @_;
224         my $cookies = fetch CGI::Cookie;
225         my $cookie = $cookies->{'auth'};
226         my( $authed, $id, $regen, $rights, $error, $name ) = checkAuthToken( $tables, $req, defined( $cookie ) ? $cookie->value : undef );
227         if( $regen ) {
228                 $req->headers_out->add( 'Set-Cookie' => new CGI::Cookie( -name => 'auth', -value => genAuthToken( $tables, $id, $req, $rights, $name ) ) );
229         }
230         my $hterror = $authed ? '' : '<div class="error"><p>'.$error.'</div>';
231         return { 'authid' => $authed ? $id : undef, 'accrights' => $rights, 'logerror' => $hterror, 'name' => $authed ? $name : undef };
232 }
233
234 sub notLoggedComplaint( $$$ ) {
235         my( $req, $args, $auth ) = @_;
236         return HTTPRedirect( $req, 'https://'.$req->hostname().$req->uri().buildArgs( $args ) ) unless $auth->{'ssl'};
237         $args->{'redirectaction'} = $args->{'action'};
238         return genLoginForm( $req, $args, 'This action requires you to be logged in', undef );
239 }
240
241 sub genResetPasswdForm( $$$$ ) {
242         my( $req, $args, $error, $values ) = @_;
243         genHtmlHead( $req, 'Reset password', undef );
244         genCustomHead( $req, $args, PciIds::Address::new( $req->uri() ), 'Reset password', [ [ 'Help', 'help', 'account' ] ], [ [ 'Log in', 'login' ], [ 'Register', 'register' ] ] );
245         print "<p>If you forgot your password (or didn't create one yet), you can reset it to a new value here.\n";
246         print "Provide your email address here and further instructions will be sent to you.\n";
247         print '<div class="error">'.$error.'</div>' if( defined $error );
248         print '<form name="respass" id="respass" method="POST" action="">
249                 <table>';
250         genForm( [ [ 'Email:', 'text', 'email', 'maxlength="255"' ],
251                 [ '', 'submit', 'respass', 'value="Send"' ] ], $values );
252         print '</table></form>';
253         genHtmlTail();
254         return OK;
255 }
256
257 sub resetPasswdForm( $$$$ ) {
258         my( $req, $args ) = @_;
259         return genResetPasswdForm( $req, $args, undef, {} );
260 }
261
262 sub resetPasswdFormSubmit( $$$ ) {
263         my( $req, $args, $tables ) = @_;
264         my( $data, $error ) = getForm( {
265                 'email' => undef
266         }, [] );
267         my( $id, $login, $passwd ) = $tables->resetInfo( $data->{'email'} );
268         if( defined( $id ) ) {
269                 $login = '' unless( defined( $login ) );
270                 my $site = $req->hostname();
271                 my $url = 'https://'.$req->hostname().setAddrPrefix( $req->uri(), 'mods' );
272                 my $hash = genResetHash( $id, $data->{'email'}, $login, $passwd );
273                 sendMail( $data->{'email'}, 'Reset password',
274                         "A request to reset password for the $site site was received for this address\n".
275                         "If you really wish to get a new password, visit this link:\n\n".
276                         $url.'?action=respass-confirm?email='.$data->{'email'}.'?confirm='.$hash."\n".
277                         "\n\nThank you\n".
278                         "\n(This is an autogenerated email, do not respond to it)" );
279                 genHtmlHead( $req, 'Reset password', undef );
280                 genCustomHead( $req, $args, PciIds::Address::new( $req->uri() ), 'Reset password', [ [ 'Help', 'help', 'account' ] ], [ [ 'Log in', 'login' ] ] );
281                 print "<p>An email with information has been sent to your address.\n";
282                 genHtmlTail();
283                 return OK;
284         } else {
285                 $error = '<p>This email address is not registered. Check it for typos or register it.';
286         }
287         return genResetPasswdForm( $req, $args, $error, $data ) if( defined( $error ) );
288 }
289
290 sub genResetPasswdConfigForm( $$$$$$ ) {
291         my( $req, $args, $error, $values, $email, $hash ) = @_;
292         genHtmlHead( $req, 'Reset password', undef );
293         genCustomHead( $req, $args, PciIds::Address::new( $req->uri() ), 'Reset password', [ [ 'Help', 'help', 'account' ] ], [ [ 'Log in', 'login' ] ] );
294         print '<div class="error">'.$error.'</div>' if( defined $error );
295         print "<p>You can enter new password here:\n";
296         print '<form name="respass-confirm" id="respass-confirm" method="POST" action="">
297                 <table>';
298         genForm( [ [ 'Password:', 'password', 'password' ],
299                 [ 'Confirm password:', 'password', 'confirm_password' ],
300                 [ '', 'submit', 'respass', 'value="Send"' ] ], $values );
301         print "</table>";
302         print "<input type='hidden' name='email' value='".encode( $email )."'><input type='hidden' name='hash' value='".encode( $hash )."'>\n";
303         print "</form>\n";
304         genHtmlTail();
305         return OK;
306 }
307
308 sub resetPasswdConfirmForm( $$$$ ) {
309         my( $req, $args, $tables, $auth ) = @_;
310         my( $email, $hash ) = ( $args->{'email'}, $args->{'confirm'} );
311         my( $id, $login, $passwd ) = $tables->resetInfo( $email );
312         my $myHash;
313         return HTTPRedirect( $req, 'https://'.$req->hostname().$req->uri().buildArgs( $args ) ) unless $auth->{'ssl'};
314         $myHash = genResetHash( $id, $email, $login, $passwd ) if( defined( $id ) );
315         if( defined( $myHash ) && ( $myHash eq $hash ) ) {#Ok, it is his mail and he asked
316                 return genResetPasswdConfigForm( $req, $args, undef, {}, $email, $hash );
317         } else {
318                 genHtmlHead( $req, 'Reset password', undef );
319                 print "<h1>Reset password</h1>\n";
320                 print "<p>Provided link is not valid. Did you use it already?\n";
321                 print "<p>You can get a <a href='".$req->uri()."?action=respass'>new one</a>.\n";
322                 genHtmlTail();
323                 return OK;
324         }
325 }
326
327 sub resetPasswdConfirmFormSubmit( $$$ ) {
328         my( $req, $args, $tables ) = @_;
329         my( $data, $error ) = getForm( {
330                 'password' => \&passLenCheck,
331                 'confirm_password' => undef,
332                 'email' => undef,
333                 'hash' => undef
334         }, [ \&passSameCheck ] );
335         my( $email, $hash ) = ( $data->{'email'}, $args->{'confirm'} );
336         if( defined( $error ) ) {
337                 return genResetPasswdConfigForm( $req, $args, $error, $data, $email, $hash );
338         } else {
339                 my( $id, $login, $passwd ) = $tables->resetInfo( $email );
340                 my $myHash;
341                 $myHash = genResetHash( $id, $email, $login, $passwd ) if( defined( $id ) );
342                 if( defined( $myHash ) && ( $myHash eq $hash ) ) {
343                         changePasswd( $tables, $id, $data->{'password'}, $email );
344                         genHtmlHead( $req, 'Reset password', undef );
345                         genCustomHead( $req, $args, PciIds::Address::new( $req->uri() ), 'Reset password', [ [ 'Help', 'help', 'account' ] ], [ [ 'Log in', 'login' ] ] );
346                         print "<p>Your password was successfuly changed.\n";
347                         genHtmlTail();
348                         return OK;
349                 } else {
350                         return genResetPasswdConfigForm( $req, $args, $error, $data, $email, $hash );
351                 }
352         }
353 }
354
355 sub genProfileForm( $$$$$$ ) {
356         my( $req, $args, $auth, $error, $data, $info ) = @_;
357         genHtmlHead( $req, 'User profile', undef );
358         delete $data->{'current_password'};
359         delete $data->{'confirm_password'};
360         delete $data->{'password'};
361         genCustomHead( $req, $args, PciIds::Address::new( $req->uri() ), 'User profile', [ [ 'Help', 'help', 'profile' ] ], [ logItem( $auth ), [ 'Notifications', 'notifications' ] ] );
362         print '<div class="error"><p>'.$error.'</div>' if defined $error;
363         print "<div class='info'><p>$info</div>\n" if defined $info;
364         print '<form name="profile" id="profile" method="POST" action=""><table>';
365         genForm( [ [ 'Email:', 'text', 'email', 'maxlength="255"' ],
366                 [ 'Login:', 'text', 'login', 'maxlength="50"' ],
367                 [ 'Xmpp:', 'text', 'xmpp', 'maxlength="255"' ],
368                 [ 'New password:', 'password', 'password' ],
369                 [ 'Confirm password:', 'password', 'confirm_password' ],
370                 [ 'Current password:', 'password', 'current_password' ],
371                 [ 'Email batch time (min):', 'text', 'email_time', 'maxlength="10"' ],
372                 [ 'Xmpp batch time (min):', 'text', 'xmpp_time', 'maxlength="10"' ],
373                 [ '', 'submit', 'profile', 'value="Submit"' ] ], $data );
374         print '</table></form>';
375         genHtmlTail();
376         return OK;
377 }
378
379 sub profileForm( $$$$ ) {
380         my( $req, $args, $tables, $auth ) = @_;
381         return notLoggedComplaint( $req, $args, $auth ) unless defined $auth->{'authid'};
382         return HTTPRedirect( $req, 'https://'.$req->hostname().$req->uri().buildArgs( $args ) ) unless $auth->{'ssl'};
383         return genProfileForm( $req, $args, $auth, undef, $tables->profileData( $auth->{'authid'} ), undef );
384 }
385
386 sub checkNum( $$ ) {
387         my( $value, $name ) = @_;
388         return ( "$name has invalid number format", '0' ) unless ( $value =~ /\d+/ );
389         return undef;
390 }
391
392 sub profileFormSubmit( $$$$ ) {
393         my( $req, $args, $tables, $auth ) = @_;
394         return notLoggedComplaint( $req, $args, $auth ) unless defined $auth->{'authid'};
395         my $oldData = $tables->profileData( $auth->{'authid'} );
396         my( $data, $error ) = getForm( {
397                 'email' => sub {
398                         my $email = shift;
399                         return undef if ( defined $email ) && ( $email eq $oldData->{'email'} );
400                         return emailCheck( $email, $tables );
401                 },
402                 'login' => sub {
403                         my $login = shift;
404                         $login = undef if ( defined $login ) && ( $login eq '' );
405                         return undef if ( defined $login ) && ( defined $oldData->{'login'} ) && ( $oldData->{'login'} eq $login );
406                         return ( undef, $login ) if ( !defined $login ) && ( !defined $oldData->{'login'} );
407                         return loginCheck( $login, $tables );
408                 },
409                 'xmpp' => sub {
410                         my $xmpp = shift;
411                         return ( undef, undef ) if ( !defined $xmpp ) || ( $xmpp eq '' );
412                         return "Xmpp address limit is 255" if length $xmpp > 255;
413                         return "Invalid Xmpp address" unless $xmpp =~ /^([^'"\@<>\/]+\@)?[^\@'"<>\/]+(\/.*)?/;
414                         return undef;
415                 },
416                 'password' => sub {
417                         my $passwd = shift;
418                         $passwd = undef if ( defined $passwd ) && ( $passwd eq '' );
419                         return ( undef, undef ) unless defined $passwd;
420                         return passLenCheck( $passwd );
421                 },
422                 'confirm_password' => undef,
423                 'current_password' => undef,
424                 'email_time' => sub {
425                         return checkNum( shift, "Email batch time" );
426                 },
427                 'xmpp_time' => sub {
428                         return checkNum( shift, "Xmpp batch time" );
429                 }
430         }, [ sub {
431                 my $data = shift;
432                 return undef unless defined $data->{'password'};
433                 return passSameCheck( $data );
434         }, sub {
435                 my $data = shift;
436                 my $change = 0;
437                 $change = 1 if $data->{'email'} ne $oldData->{'email'};
438                 $change = 1 if ( ( ( defined $data->{'login'} ) != ( defined $oldData->{'login'} ) ) || ( ( defined $data->{'login'} ) && ( defined $oldData->{'login'} ) && ( $data->{'login'} ne $oldData->{'login'} ) ) );
439                 $change = 1 if ( defined $data->{'password'} ) && ( $data->{'password'} ne '' );
440                 return undef unless $change;
441                 my $logged = 0;
442                 my( $id, $passwd, $email, $last ) = $tables->getLogInfo( $oldData->{'email'} );
443                 if( defined $passwd && defined $data->{'current_password'} ) {
444                         my $salted = saltedPasswd( $email, $data->{'current_password'} );
445                         $logged = ( $salted eq $passwd ) && ( $id == $auth->{'authid'} );
446                 }
447                 return "You need to provide correct current password to change email, login or password" unless $logged;
448                 return undef;
449         } ] );
450         return genProfileForm( $req, $args, $auth, $error, $data, undef ) if defined $error;
451         pushProfile( $tables, $auth->{'authid'}, $oldData, $data );
452         return genProfileForm( $req, $args, $auth, undef, $data, "Profile updated." );
453 }
454
455 1;