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