Bug 13757: OPAC changes
[koha.git] / opac / opac-memberentry.pl
1 #!/usr/bin/perl
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18 use Modern::Perl;
19
20 use CGI qw ( -utf8 );
21 use Digest::MD5 qw( md5_base64 md5_hex );
22 use Encode qw( encode );
23 use List::MoreUtils qw( each_array uniq );
24 use String::Random qw( random_string );
25
26 use C4::Auth;
27 use C4::Output;
28 use C4::Members;
29 use C4::Form::MessagingPreferences;
30 use Koha::Patrons;
31 use Koha::Patron::Modification;
32 use Koha::Patron::Modifications;
33 use C4::Scrubber;
34 use Email::Valid;
35 use Koha::DateUtils;
36 use Koha::Libraries;
37 use Koha::Patron::Images;
38 use Koha::Token;
39
40 my $cgi = new CGI;
41 my $dbh = C4::Context->dbh;
42
43 my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
44     {
45         template_name   => "opac-memberentry.tt",
46         type            => "opac",
47         query           => $cgi,
48         authnotrequired => 1,
49     }
50 );
51
52 unless ( C4::Context->preference('PatronSelfRegistration') || $borrowernumber )
53 {
54     print $cgi->redirect("/cgi-bin/koha/opac-main.pl");
55     exit;
56 }
57
58 my $action = $cgi->param('action') || q{};
59 if ( $action eq q{} ) {
60     if ($borrowernumber) {
61         $action = 'edit';
62     }
63     else {
64         $action = 'new';
65     }
66 }
67
68 my $mandatory = GetMandatoryFields($action);
69
70 my @libraries = Koha::Libraries->search;
71 if ( my @libraries_to_display = split '\|', C4::Context->preference('PatronSelfRegistrationLibraryList') ) {
72     @libraries = map { my $b = $_; my $branchcode = $_->branchcode; grep( /^$branchcode$/, @libraries_to_display ) ? $b : () } @libraries;
73 }
74 my ( $min, $max ) = C4::Members::get_cardnumber_length();
75 if ( defined $min ) {
76      $template->param(
77          minlength_cardnumber => $min,
78          maxlength_cardnumber => $max
79      );
80  }
81
82 $template->param(
83     action            => $action,
84     hidden            => GetHiddenFields( $mandatory, 'registration' ),
85     mandatory         => $mandatory,
86     libraries         => \@libraries,
87     OPACPatronDetails => C4::Context->preference('OPACPatronDetails'),
88 );
89
90 my $attributes = ParsePatronAttributes($cgi);
91 my $conflicting_attribute = 0;
92
93 foreach my $attr (@$attributes) {
94     unless ( C4::Members::Attributes::CheckUniqueness($attr->{code}, $attr->{value}, $borrowernumber) ) {
95         my $attr_info = C4::Members::AttributeTypes->fetch($attr->{code});
96         $template->param(
97             extended_unique_id_failed_code => $attr->{code},
98             extended_unique_id_failed_value => $attr->{value},
99             extended_unique_id_failed_description => $attr_info->description()
100         );
101         $conflicting_attribute = 1;
102     }
103 }
104
105 if ( $action eq 'create' ) {
106
107     my %borrower = ParseCgiForBorrower($cgi);
108
109     %borrower = DelEmptyFields(%borrower);
110
111     my @empty_mandatory_fields = CheckMandatoryFields( \%borrower, $action );
112     my $invalidformfields = CheckForInvalidFields(\%borrower);
113     delete $borrower{'password2'};
114     my $cardnumber_error_code;
115     if ( !grep { $_ eq 'cardnumber' } @empty_mandatory_fields ) {
116         # No point in checking the cardnumber if it's missing and mandatory, it'll just generate a
117         # spurious length warning.
118         $cardnumber_error_code = checkcardnumber( $borrower{cardnumber}, $borrower{borrowernumber} );
119     }
120
121     if ( @empty_mandatory_fields || @$invalidformfields || $cardnumber_error_code || $conflicting_attribute ) {
122         if ( $cardnumber_error_code == 1 ) {
123             $template->param( cardnumber_already_exists => 1 );
124         } elsif ( $cardnumber_error_code == 2 ) {
125             $template->param( cardnumber_wrong_length => 1 );
126         }
127
128         $template->param(
129             empty_mandatory_fields => \@empty_mandatory_fields,
130             invalid_form_fields    => $invalidformfields,
131             borrower               => \%borrower
132         );
133         $template->param( patron_attribute_classes => GeneratePatronAttributesForm( undef, $attributes ) );
134     }
135     elsif (
136         md5_base64( uc( $cgi->param('captcha') ) ) ne $cgi->param('captcha_digest') )
137     {
138         $template->param(
139             failed_captcha => 1,
140             borrower       => \%borrower
141         );
142         $template->param( patron_attribute_classes => GeneratePatronAttributesForm( undef, $attributes ) );
143     }
144     else {
145         if (
146             C4::Context->boolean_preference(
147                 'PatronSelfRegistrationVerifyByEmail')
148           )
149         {
150             ( $template, $borrowernumber, $cookie ) = get_template_and_user(
151                 {
152                     template_name   => "opac-registration-email-sent.tt",
153                     type            => "opac",
154                     query           => $cgi,
155                     authnotrequired => 1,
156                 }
157             );
158             $template->param( 'email' => $borrower{'email'} );
159
160             my $verification_token = md5_hex( time().{}.rand().{}.$$ );
161             while ( Koha::Patron::Modifications->search( { verification_token => $verification_token } )->count() ) {
162                 $verification_token = md5_hex( time().{}.rand().{}.$$ );
163             }
164
165             $borrower{password}           = random_string("..........");
166             $borrower{verification_token} = $verification_token;
167
168             Koha::Patron::Modification->new( \%borrower )->store();
169
170             #Send verification email
171             my $letter = C4::Letters::GetPreparedLetter(
172                 module      => 'members',
173                 letter_code => 'OPAC_REG_VERIFY',
174                 tables      => {
175                     borrower_modifications => $verification_token,
176                 },
177             );
178
179             C4::Letters::EnqueueLetter(
180                 {
181                     letter                 => $letter,
182                     message_transport_type => 'email',
183                     to_address             => $borrower{'email'},
184                     from_address =>
185                       C4::Context->preference('KohaAdminEmailAddress'),
186                 }
187             );
188         }
189         else {
190             ( $template, $borrowernumber, $cookie ) = get_template_and_user(
191                 {
192                     template_name   => "opac-registration-confirmation.tt",
193                     type            => "opac",
194                     query           => $cgi,
195                     authnotrequired => 1,
196                 }
197             );
198
199             $template->param( OpacPasswordChange =>
200                   C4::Context->preference('OpacPasswordChange') );
201
202             my ( $borrowernumber, $password ) = AddMember_Opac(%borrower);
203             C4::Members::Attributes::SetBorrowerAttributes( $borrowernumber, $attributes );
204             C4::Form::MessagingPreferences::handle_form_action($cgi, { borrowernumber => $borrowernumber }, $template, 1, C4::Context->preference('PatronSelfRegistrationDefaultCategory') ) if $borrowernumber && C4::Context->preference('EnhancedMessagingPreferences');
205
206             $template->param( password_cleartext => $password );
207             $template->param(
208                 borrower => GetMember( borrowernumber => $borrowernumber ) );
209             $template->param(
210                 PatronSelfRegistrationAdditionalInstructions =>
211                   C4::Context->preference(
212                     'PatronSelfRegistrationAdditionalInstructions')
213             );
214         }
215     }
216 }
217 elsif ( $action eq 'update' ) {
218
219     my $borrower = GetMember( borrowernumber => $borrowernumber );
220     die "Wrong CSRF token"
221         unless Koha::Token->new->check_csrf({
222             id     => Encode::encode( 'UTF-8', $borrower->{userid} ),
223             secret => md5_base64( Encode::encode( 'UTF-8', C4::Context->config('pass') ) ),
224             token  => scalar $cgi->param('csrf_token'),
225         });
226
227     my %borrower = ParseCgiForBorrower($cgi);
228
229     my %borrower_changes = DelEmptyFields(%borrower);
230     my @empty_mandatory_fields =
231       CheckMandatoryFields( \%borrower_changes, $action );
232     my $invalidformfields = CheckForInvalidFields(\%borrower);
233
234     # Send back the data to the template
235     %borrower = ( %$borrower, %borrower );
236
237     if (@empty_mandatory_fields || @$invalidformfields) {
238         $template->param(
239             empty_mandatory_fields => \@empty_mandatory_fields,
240             invalid_form_fields    => $invalidformfields,
241             borrower               => \%borrower,
242             csrf_token             => Koha::Token->new->generate_csrf({
243                 id     => Encode::encode( 'UTF-8', $borrower->{userid} ),
244                 secret => md5_base64( Encode::encode( 'UTF-8', C4::Context->config('pass') ) ),
245             }),
246         );
247         $template->param( patron_attribute_classes => GeneratePatronAttributesForm( undef, $attributes ) );
248
249         $template->param( action => 'edit' );
250     }
251     else {
252         my %borrower_changes = DelUnchangedFields( $borrowernumber, %borrower );
253         if (%borrower_changes) {
254             ( $template, $borrowernumber, $cookie ) = get_template_and_user(
255                 {
256                     template_name   => "opac-memberentry-update-submitted.tt",
257                     type            => "opac",
258                     query           => $cgi,
259                     authnotrequired => 1,
260                 }
261             );
262
263             $borrower_changes{borrowernumber} = $borrowernumber;
264
265             # FIXME update the following with
266             # Koha::Patron::Modifications->search({ borrowernumber => $borrowernumber })->delete;
267             # when bug 17091 will be pushed
268             my $patron_modifications = Koha::Patron::Modifications->search({ borrowernumber => $borrowernumber });
269             while ( my $patron_modification = $patron_modifications->next ) {
270                 $patron_modification->delete;
271             }
272
273             my $m = Koha::Patron::Modification->new( \%borrower_changes )->store();
274
275             $template->param(
276                 borrower => GetMember( borrowernumber => $borrowernumber ),
277             );
278         }
279         else {
280             $template->param(
281                 action => 'edit',
282                 nochanges => 1,
283                 borrower => GetMember( borrowernumber => $borrowernumber ),
284                 csrf_token => Koha::Token->new->generate_csrf({
285                     id     => Encode::encode( 'UTF-8', $borrower->{userid} ),
286                     secret => md5_base64( Encode::encode( 'UTF-8', C4::Context->config('pass') ) ),
287                 }),
288             );
289         }
290     }
291 }
292 elsif ( $action eq 'edit' ) {    #Display logged in borrower's data
293     my $borrower = GetMember( borrowernumber => $borrowernumber );
294
295     $template->param(
296         borrower  => $borrower,
297         guarantor => scalar Koha::Patrons->find($borrowernumber)->guarantor(),
298         hidden => GetHiddenFields( $mandatory, 'modification' ),
299         csrf_token => Koha::Token->new->generate_csrf({
300             id     => Encode::encode( 'UTF-8', $borrower->{userid} ),
301             secret => md5_base64( Encode::encode( 'UTF-8', C4::Context->config('pass') ) ),
302         }),
303     );
304
305     if (C4::Context->preference('OPACpatronimages')) {
306         my $patron_image = Koha::Patron::Images->find($borrower->{borrowernumber});
307         $template->param( display_patron_image => 1 ) if $patron_image;
308     }
309
310     $template->param( patron_attribute_classes => GeneratePatronAttributesForm( $borrower ) );
311 } else {
312     $template->param( patron_attribute_classes => GeneratePatronAttributesForm() );
313 }
314
315 my $captcha = random_string("CCCCC");
316
317 $template->param(
318     captcha        => $captcha,
319     captcha_digest => md5_base64($captcha)
320 );
321
322 output_html_with_http_headers $cgi, $cookie, $template->output, undef, { force_no_caching => 1 };
323
324 sub GetHiddenFields {
325     my ( $mandatory, $action ) = @_;
326     my %hidden_fields;
327
328     my $BorrowerUnwantedField = $action eq 'modification' ?
329       C4::Context->preference( "PatronSelfModificationBorrowerUnwantedField" ) :
330       C4::Context->preference( "PatronSelfRegistrationBorrowerUnwantedField" );
331
332     my @fields = split( /\|/, $BorrowerUnwantedField || q|| );
333     foreach (@fields) {
334         next unless m/\w/o;
335         #Don't hide mandatory fields
336         next if $mandatory->{$_};
337         $hidden_fields{$_} = 1;
338     }
339
340     return \%hidden_fields;
341 }
342
343 sub GetMandatoryFields {
344     my ($action) = @_;
345
346     my %mandatory_fields;
347
348     my $BorrowerMandatoryField =
349       C4::Context->preference("PatronSelfRegistrationBorrowerMandatoryField");
350
351     my @fields = split( /\|/, $BorrowerMandatoryField );
352
353     foreach (@fields) {
354         $mandatory_fields{$_} = 1;
355     }
356
357     if ( $action eq 'create' || $action eq 'new' ) {
358         $mandatory_fields{'email'} = 1
359           if C4::Context->boolean_preference(
360             'PatronSelfRegistrationVerifyByEmail');
361     }
362
363     return \%mandatory_fields;
364 }
365
366 sub CheckMandatoryFields {
367     my ( $borrower, $action ) = @_;
368
369     my @empty_mandatory_fields;
370
371     my $mandatory_fields = GetMandatoryFields($action);
372     delete $mandatory_fields->{'cardnumber'};
373
374     foreach my $key ( keys %$mandatory_fields ) {
375         push( @empty_mandatory_fields, $key )
376           unless ( defined( $borrower->{$key} ) && $borrower->{$key} );
377     }
378
379     return @empty_mandatory_fields;
380 }
381
382 sub CheckForInvalidFields {
383     my $minpw = C4::Context->preference('minPasswordLength');
384     my $borrower = shift;
385     my @invalidFields;
386     if ($borrower->{'email'}) {
387         unless ( Email::Valid->address($borrower->{'email'}) ) {
388             push(@invalidFields, "email");
389         } elsif ( C4::Context->preference("PatronSelfRegistrationEmailMustBeUnique") ) {
390             my $patrons_with_same_email = Koha::Patrons->search( { email => $borrower->{email} })->count;
391             if ( $patrons_with_same_email ) {
392                 push @invalidFields, "duplicate_email";
393             }
394         }
395     }
396     if ($borrower->{'emailpro'}) {
397         push(@invalidFields, "emailpro") if (!Email::Valid->address($borrower->{'emailpro'}));
398     }
399     if ($borrower->{'B_email'}) {
400         push(@invalidFields, "B_email") if (!Email::Valid->address($borrower->{'B_email'}));
401     }
402     if ( $borrower->{'password'} ne $borrower->{'password2'} ){
403         push(@invalidFields, "password_match");
404     }
405     if ( $borrower->{'password'}  && $minpw && (length($borrower->{'password'}) < $minpw) ) {
406        push(@invalidFields, "password_invalid");
407     }
408     if ( $borrower->{'password'} ) {
409        push(@invalidFields, "password_spaces") if ($borrower->{'password'} =~ /^\s/ or $borrower->{'password'} =~ /\s$/);
410     }
411
412     return \@invalidFields;
413 }
414
415 sub ParseCgiForBorrower {
416     my ($cgi) = @_;
417
418     my $scrubber = C4::Scrubber->new();
419     my %borrower;
420
421     foreach ( $cgi->param ) {
422         if ( $_ =~ '^borrower_' ) {
423             my ($key) = substr( $_, 9 );
424             $borrower{$key} = $scrubber->scrub( scalar $cgi->param($_) );
425         }
426     }
427
428     my $dob_dt;
429     $dob_dt = eval { dt_from_string( $borrower{'dateofbirth'} ); }
430         if ( $borrower{'dateofbirth'} );
431
432     if ( $dob_dt ) {
433         $borrower{'dateofbirth'} = output_pref ( { dt => $dob_dt, dateonly => 1, dateformat => 'iso' } );
434     }
435     else {
436         # Trigger validation
437         $borrower{'dateofbirth'} = undef;
438     }
439
440     return %borrower;
441 }
442
443 sub DelUnchangedFields {
444     my ( $borrowernumber, %new_data ) = @_;
445
446     my $current_data = GetMember( borrowernumber => $borrowernumber );
447
448     foreach my $key ( keys %new_data ) {
449         if ( $current_data->{$key} eq $new_data{$key} ) {
450             delete $new_data{$key};
451         }
452     }
453
454     return %new_data;
455 }
456
457 sub DelEmptyFields {
458     my (%borrower) = @_;
459
460     foreach my $key ( keys %borrower ) {
461         delete $borrower{$key} unless $borrower{$key};
462     }
463
464     return %borrower;
465 }
466
467 sub GeneratePatronAttributesForm {
468     my ( $borrower, $entered_attributes ) = @_;
469
470     # Get all attribute types and the values for this patron (if applicable)
471     my @types = C4::Members::AttributeTypes::GetAttributeTypes();
472
473     if (scalar(@types) == 0) {
474         return [];
475     }
476
477     my %attr_values = ();
478
479     if ( $borrower ) {
480         my $attributes = C4::Members::Attributes::GetBorrowerAttributes($borrowernumber);
481
482         # Remap the patron's attributes into a hash of arrayrefs per attribute (depends on
483         # autovivification)
484         foreach my $attr (@$attributes) {
485             push @{ $attr_values{ $attr->{code} } }, $attr;
486         }
487     }
488
489     if ( $entered_attributes ) {
490         foreach my $attr (@$entered_attributes) {
491             push @{ $attr_values{ $attr->{code} } }, $attr;
492         }
493     }
494
495     # Find all existing classes
496     my @classes = uniq( map { $_->{class} } @types );
497     @classes = sort @classes;
498     my %items_by_class;
499
500     foreach my $attr_type_desc (@types) {
501         my $attr_type = C4::Members::AttributeTypes->fetch( $attr_type_desc->{code} );
502         # Make sure this attribute should be displayed in the OPAC
503         next unless ( $attr_type->opac_display() );
504         # Then, make sure it either has values or is editable
505         next unless ( $attr_values{ $attr_type->code() } || $attr_type->opac_editable() );
506
507         push @{ $items_by_class{ $attr_type->class() } }, {
508             type => $attr_type,
509             # If editable, make sure there's at least one empty entry, to make the template's job easier
510             values => $attr_values{ $attr_type->code() } || [{}]
511         };
512     }
513
514     # Finally, build a list of containing classes
515     my @class_loop;
516     foreach my $class (@classes) {
517         next unless ( $items_by_class{$class} );
518
519         my $av = Koha::AuthorisedValues->search({ category => 'PA_CLASS', authorised_value => $class });
520         my $lib = $av->count ? $av->next->opac_description : $class;
521
522         push @class_loop, {
523             class => $class,
524             items => $items_by_class{$class},
525             lib   => $lib,
526         };
527     }
528
529     return \@class_loop;
530 }
531
532 sub ParsePatronAttributes {
533     my ( $cgi ) = @_;
534
535     my @codes = $cgi->param('patron_attribute_code');
536     my @values = $cgi->param('patron_attribute_value');
537     my @passwords = $cgi->param('patron_attribute_password');
538
539     my $ea = each_array( @codes, @values, @passwords );
540     my @attributes;
541     my %dups = ();
542
543     while ( my ( $code, $value, $password ) = $ea->() ) {
544         next unless defined($value) and $value ne '';
545         next if exists $dups{$code}->{$value};
546         $dups{$code}->{$value} = 1;
547
548         push @attributes, { code => $code, value => $value, password => $password };
549     }
550
551     return \@attributes;
552 }