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