Bug 25137: (bug 23084 follow-up) Fix incorrect grep ternary condition
[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 JSON;
23 use List::MoreUtils qw( any 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::AuthUtils;
31 use Koha::Patrons;
32 use Koha::Patron::Consent;
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::Attribute::Types;
40 use Koha::Patron::Attributes;
41 use Koha::Patron::Images;
42 use Koha::Patron::Modification;
43 use Koha::Patron::Modifications;
44 use Koha::Patrons;
45 use Koha::Token;
46
47 my $cgi = new CGI;
48 my $dbh = C4::Context->dbh;
49
50 my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
51     {
52         template_name   => "opac-memberentry.tt",
53         type            => "opac",
54         query           => $cgi,
55         authnotrequired => 1,
56     }
57 );
58
59 unless ( C4::Context->preference('PatronSelfRegistration') || $borrowernumber )
60 {
61     print $cgi->redirect("/cgi-bin/koha/opac-main.pl");
62     exit;
63 }
64
65 my $action = $cgi->param('action') || q{};
66 if ( $action eq q{} ) {
67     if ($borrowernumber) {
68         $action = 'edit';
69     }
70     else {
71         $action = 'new';
72     }
73 }
74
75 my $mandatory = GetMandatoryFields($action);
76
77 my @libraries = Koha::Libraries->search;
78 if ( my @libraries_to_display = split '\|', C4::Context->preference('PatronSelfRegistrationLibraryList') ) {
79     @libraries = map {
80         my $b          = $_;
81         my $branchcode = $_->branchcode;
82         ( grep { $_ eq $branchcode } @libraries_to_display ) ? $b : ()
83     } @libraries;
84 }
85 my ( $min, $max ) = C4::Members::get_cardnumber_length();
86 if ( defined $min ) {
87      $template->param(
88          minlength_cardnumber => $min,
89          maxlength_cardnumber => $max
90      );
91  }
92
93 $template->param(
94     action            => $action,
95     hidden            => GetHiddenFields( $mandatory, $action ),
96     mandatory         => $mandatory,
97     libraries         => \@libraries,
98     OPACPatronDetails => C4::Context->preference('OPACPatronDetails'),
99 );
100
101 my $attributes = ParsePatronAttributes($borrowernumber,$cgi);
102 my $conflicting_attribute = 0;
103
104 foreach my $attr (@$attributes) {
105     my $attribute = Koha::Patron::Attribute->new($attr);
106     eval {$attribute->check_unique_id};
107     if ( $@ ) {
108         my $attr_type = Koha::Patron::Attribute::Types->find($attr->{code});
109         $template->param(
110             extended_unique_id_failed_code => $attr->{code},
111             extended_unique_id_failed_value => $attr->{attribute},
112             extended_unique_id_failed_description => $attr_type->description,
113         );
114         $conflicting_attribute = 1;
115     }
116 }
117
118 if ( $action eq 'create' ) {
119
120     my %borrower = ParseCgiForBorrower($cgi);
121
122     %borrower = DelEmptyFields(%borrower);
123
124     my @empty_mandatory_fields = CheckMandatoryFields( \%borrower, $action );
125     my $invalidformfields = CheckForInvalidFields(\%borrower);
126     delete $borrower{'password2'};
127     my $cardnumber_error_code;
128     if ( !grep { $_ eq 'cardnumber' } @empty_mandatory_fields ) {
129         # No point in checking the cardnumber if it's missing and mandatory, it'll just generate a
130         # spurious length warning.
131         $cardnumber_error_code = checkcardnumber( $borrower{cardnumber}, $borrower{borrowernumber} );
132     }
133
134     if ( @empty_mandatory_fields || @$invalidformfields || $cardnumber_error_code || $conflicting_attribute ) {
135         if ( $cardnumber_error_code == 1 ) {
136             $template->param( cardnumber_already_exists => 1 );
137         } elsif ( $cardnumber_error_code == 2 ) {
138             $template->param( cardnumber_wrong_length => 1 );
139         }
140
141         $template->param(
142             empty_mandatory_fields => \@empty_mandatory_fields,
143             invalid_form_fields    => $invalidformfields,
144             borrower               => \%borrower
145         );
146         $template->param( patron_attribute_classes => GeneratePatronAttributesForm( undef, $attributes ) );
147     }
148     elsif (
149         md5_base64( uc( $cgi->param('captcha') ) ) ne $cgi->param('captcha_digest') )
150     {
151         $template->param(
152             failed_captcha => 1,
153             borrower       => \%borrower
154         );
155         $template->param( patron_attribute_classes => GeneratePatronAttributesForm( undef, $attributes ) );
156     }
157     else {
158         if (
159             C4::Context->boolean_preference(
160                 'PatronSelfRegistrationVerifyByEmail')
161           )
162         {
163             ( $template, $borrowernumber, $cookie ) = get_template_and_user(
164                 {
165                     template_name   => "opac-registration-email-sent.tt",
166                     type            => "opac",
167                     query           => $cgi,
168                     authnotrequired => 1,
169                 }
170             );
171             $template->param( 'email' => $borrower{'email'} );
172
173             my $verification_token = md5_hex( time().{}.rand().{}.$$ );
174             while ( Koha::Patron::Modifications->search( { verification_token => $verification_token } )->count() ) {
175                 $verification_token = md5_hex( time().{}.rand().{}.$$ );
176             }
177
178             $borrower{password}          = Koha::AuthUtils::generate_password unless $borrower{password};
179             $borrower{verification_token} = $verification_token;
180
181             Koha::Patron::Modification->new( \%borrower )->store();
182
183             #Send verification email
184             my $letter = C4::Letters::GetPreparedLetter(
185                 module      => 'members',
186                 letter_code => 'OPAC_REG_VERIFY',
187                 lang        => 'default', # Patron does not have a preferred language defined yet
188                 tables      => {
189                     borrower_modifications => $verification_token,
190                 },
191             );
192
193             C4::Letters::EnqueueLetter(
194                 {
195                     letter                 => $letter,
196                     message_transport_type => 'email',
197                     to_address             => $borrower{'email'},
198                     from_address =>
199                       C4::Context->preference('KohaAdminEmailAddress'),
200                 }
201             );
202             my $num_letters_attempted = C4::Letters::SendQueuedMessages( {
203                     letter_code => 'OPAC_REG_VERIFY'
204                     } );
205         }
206         else {
207             ( $template, $borrowernumber, $cookie ) = get_template_and_user(
208                 {
209                     template_name   => "opac-registration-confirmation.tt",
210                     type            => "opac",
211                     query           => $cgi,
212                     authnotrequired => 1,
213                 }
214             );
215
216             $borrower{categorycode}     ||= C4::Context->preference('PatronSelfRegistrationDefaultCategory');
217             $borrower{password}         ||= Koha::AuthUtils::generate_password;
218             my $consent_dt = delete $borrower{gdpr_proc_consent};
219             my $patron = Koha::Patron->new( \%borrower )->store;
220             Koha::Patron::Consent->new({ borrowernumber => $patron->borrowernumber, type => 'GDPR_PROCESSING', given_on => $consent_dt })->store if $consent_dt;
221             if ( $patron ) {
222                 $patron->extended_attributes->filter_by_branch_limitations->delete;
223                 $patron->extended_attributes($attributes);
224                 if ( C4::Context->preference('EnhancedMessagingPreferences') ) {
225                     C4::Form::MessagingPreferences::handle_form_action(
226                         $cgi,
227                         { borrowernumber => $patron->borrowernumber },
228                         $template,
229                         1,
230                         C4::Context->preference('PatronSelfRegistrationDefaultCategory')
231                     );
232                 }
233
234                 $template->param( password_cleartext => $patron->plain_text_password );
235                 $template->param( borrower => $patron->unblessed );
236             } else {
237                 # FIXME Handle possible errors here
238             }
239             $template->param(
240                 PatronSelfRegistrationAdditionalInstructions =>
241                   C4::Context->preference(
242                     'PatronSelfRegistrationAdditionalInstructions')
243             );
244         }
245     }
246 }
247 elsif ( $action eq 'update' ) {
248
249     my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
250     die "Wrong CSRF token"
251         unless Koha::Token->new->check_csrf({
252             session_id => scalar $cgi->cookie('CGISESSID'),
253             token  => scalar $cgi->param('csrf_token'),
254         });
255
256     my %borrower = ParseCgiForBorrower($cgi);
257     $borrower{borrowernumber} = $borrowernumber;
258
259     my @empty_mandatory_fields =
260       CheckMandatoryFields( \%borrower, $action );
261     my $invalidformfields = CheckForInvalidFields(\%borrower);
262
263     # Send back the data to the template
264     %borrower = ( %$borrower, %borrower );
265
266     if (@empty_mandatory_fields || @$invalidformfields) {
267         $template->param(
268             empty_mandatory_fields => \@empty_mandatory_fields,
269             invalid_form_fields    => $invalidformfields,
270             borrower               => \%borrower,
271             csrf_token             => Koha::Token->new->generate_csrf({
272                 session_id => scalar $cgi->cookie('CGISESSID'),
273             }),
274         );
275         $template->param( patron_attribute_classes => GeneratePatronAttributesForm( $borrowernumber, $attributes ) );
276
277         $template->param( action => 'edit' );
278     }
279     else {
280         my %borrower_changes = DelUnchangedFields( $borrowernumber, %borrower );
281         $borrower_changes{'changed_fields'} = join ',', keys %borrower_changes;
282         my $extended_attributes_changes = FilterUnchangedAttributes( $borrowernumber, $attributes );
283
284         if ( %borrower_changes || scalar @{$extended_attributes_changes} > 0 ) {
285             ( $template, $borrowernumber, $cookie ) = get_template_and_user(
286                 {
287                     template_name   => "opac-memberentry-update-submitted.tt",
288                     type            => "opac",
289                     query           => $cgi,
290                     authnotrequired => 1,
291                 }
292             );
293
294             $borrower_changes{borrowernumber} = $borrowernumber;
295             $borrower_changes{extended_attributes} = to_json($extended_attributes_changes);
296
297             Koha::Patron::Modifications->search({ borrowernumber => $borrowernumber })->delete;
298
299             my $m = Koha::Patron::Modification->new( \%borrower_changes )->store();
300
301             my $patron = Koha::Patrons->find( $borrowernumber );
302             $template->param( borrower => $patron->unblessed );
303         }
304         else {
305             my $patron = Koha::Patrons->find( $borrowernumber );
306             $template->param(
307                 action => 'edit',
308                 nochanges => 1,
309                 borrower => $patron->unblessed,
310                 patron_attribute_classes => GeneratePatronAttributesForm( $borrowernumber, $attributes ),
311                 csrf_token => Koha::Token->new->generate_csrf({
312                     session_id => scalar $cgi->cookie('CGISESSID'),
313                 }),
314             );
315         }
316     }
317 }
318 elsif ( $action eq 'edit' ) {    #Display logged in borrower's data
319     my $patron = Koha::Patrons->find( $borrowernumber );
320     my $borrower = $patron->unblessed;
321
322     $template->param(
323         borrower  => $borrower,
324         hidden => GetHiddenFields( $mandatory, 'edit' ),
325         csrf_token => Koha::Token->new->generate_csrf({
326             session_id => scalar $cgi->cookie('CGISESSID'),
327         }),
328     );
329
330     if (C4::Context->preference('OPACpatronimages')) {
331         $template->param( display_patron_image => 1 ) if $patron->image;
332     }
333
334     $template->param( patron_attribute_classes => GeneratePatronAttributesForm( $borrowernumber ) );
335 } else {
336     # Render self-registration page
337     $template->param( patron_attribute_classes => GeneratePatronAttributesForm() );
338 }
339
340 my $captcha = random_string("CCCCC");
341 my $patron_param = Koha::Patrons->find( $borrowernumber );
342 $template->param(
343     has_guarantor_flag => $patron_param->guarantor_relationships->guarantors->_resultset->count
344 ) if $patron_param;
345
346 $template->param(
347     captcha        => $captcha,
348     captcha_digest => md5_base64($captcha),
349     patron         => $patron_param
350 );
351
352 output_html_with_http_headers $cgi, $cookie, $template->output, undef, { force_no_caching => 1 };
353
354 sub GetHiddenFields {
355     my ( $mandatory, $action ) = @_;
356     my %hidden_fields;
357
358     my $BorrowerUnwantedField = $action eq 'edit' || $action eq 'update' ?
359       C4::Context->preference( "PatronSelfModificationBorrowerUnwantedField" ) :
360       C4::Context->preference( "PatronSelfRegistrationBorrowerUnwantedField" );
361
362     my @fields = split( /\|/, $BorrowerUnwantedField || q|| );
363     foreach (@fields) {
364         next unless m/\w/o;
365         #Don't hide mandatory fields
366         next if $mandatory->{$_};
367         $hidden_fields{$_} = 1;
368     }
369
370     return \%hidden_fields;
371 }
372
373 sub GetMandatoryFields {
374     my ($action) = @_;
375
376     my %mandatory_fields;
377
378     my $BorrowerMandatoryField =
379       C4::Context->preference("PatronSelfRegistrationBorrowerMandatoryField");
380
381     my @fields = split( /\|/, $BorrowerMandatoryField );
382     push @fields, 'gdpr_proc_consent' if C4::Context->preference('GDPR_Policy') && $action eq 'create';
383
384     foreach (@fields) {
385         $mandatory_fields{$_} = 1;
386     }
387
388     if ( $action eq 'create' || $action eq 'new' ) {
389         $mandatory_fields{'email'} = 1
390           if C4::Context->boolean_preference(
391             'PatronSelfRegistrationVerifyByEmail');
392     }
393
394     return \%mandatory_fields;
395 }
396
397 sub CheckMandatoryFields {
398     my ( $borrower, $action ) = @_;
399
400     my @empty_mandatory_fields;
401
402     my $mandatory_fields = GetMandatoryFields($action);
403     delete $mandatory_fields->{'cardnumber'};
404
405     foreach my $key ( keys %$mandatory_fields ) {
406         push( @empty_mandatory_fields, $key )
407           unless ( defined( $borrower->{$key} ) && $borrower->{$key} );
408     }
409
410     return @empty_mandatory_fields;
411 }
412
413 sub CheckForInvalidFields {
414     my $borrower = shift;
415     my @invalidFields;
416     if ($borrower->{'email'}) {
417         unless ( Email::Valid->address($borrower->{'email'}) ) {
418             push(@invalidFields, "email");
419         } elsif ( C4::Context->preference("PatronSelfRegistrationEmailMustBeUnique") ) {
420             my $patrons_with_same_email = Koha::Patrons->search( # FIXME Should be search_limited?
421                 {
422                     email => $borrower->{email},
423                     (
424                         exists $borrower->{borrowernumber}
425                         ? ( borrowernumber =>
426                               { '!=' => $borrower->{borrowernumber} } )
427                         : ()
428                     )
429                 }
430             )->count;
431             if ( $patrons_with_same_email ) {
432                 push @invalidFields, "duplicate_email";
433             }
434         }
435     }
436     if ($borrower->{'emailpro'}) {
437         push(@invalidFields, "emailpro") if (!Email::Valid->address($borrower->{'emailpro'}));
438     }
439     if ($borrower->{'B_email'}) {
440         push(@invalidFields, "B_email") if (!Email::Valid->address($borrower->{'B_email'}));
441     }
442     if ( defined $borrower->{'password'}
443         and $borrower->{'password'} ne $borrower->{'password2'} )
444     {
445         push( @invalidFields, "password_match" );
446     }
447     if ( $borrower->{'password'} ) {
448         my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $borrower->{password} );
449           unless ( $is_valid ) {
450               push @invalidFields, 'password_too_short' if $error eq 'too_short';
451               push @invalidFields, 'password_too_weak' if $error eq 'too_weak';
452               push @invalidFields, 'password_has_whitespaces' if $error eq 'has_whitespaces';
453           }
454     }
455
456     return \@invalidFields;
457 }
458
459 sub ParseCgiForBorrower {
460     my ($cgi) = @_;
461
462     my $scrubber = C4::Scrubber->new();
463     my %borrower;
464
465     foreach my $field ( $cgi->param ) {
466         if ( $field =~ '^borrower_' ) {
467             my ($key) = substr( $field, 9 );
468             if ( $field !~ '^borrower_password' ) {
469                 $borrower{$key} = $scrubber->scrub( scalar $cgi->param($field) );
470             } else {
471                 # Allow html characters for passwords
472                 $borrower{$key} = $cgi->param($field);
473             }
474         }
475     }
476
477     my $dob_dt;
478     $dob_dt = eval { dt_from_string( $borrower{'dateofbirth'} ); }
479         if ( $borrower{'dateofbirth'} );
480
481     if ( $dob_dt ) {
482         $borrower{'dateofbirth'} = output_pref ( { dt => $dob_dt, dateonly => 1, dateformat => 'iso' } );
483     }
484     else {
485         # Trigger validation
486         $borrower{'dateofbirth'} = undef;
487     }
488
489     # Replace checkbox 'agreed' by datetime in gdpr_proc_consent
490     $borrower{gdpr_proc_consent} = dt_from_string if  $borrower{gdpr_proc_consent} && $borrower{gdpr_proc_consent} eq 'agreed';
491
492     return %borrower;
493 }
494
495 sub DelUnchangedFields {
496     my ( $borrowernumber, %new_data ) = @_;
497     # get the mandatory fields so we can get the hidden fields
498     my $mandatory = GetMandatoryFields('edit');
499     my $patron = Koha::Patrons->find( $borrowernumber );
500     my $current_data = $patron->unblessed;
501     # get the hidden fields so we don't obliterate them should they have data patrons aren't allowed to modify
502     my $hidden_fields = GetHiddenFields($mandatory, 'edit');
503
504
505     foreach my $key ( keys %new_data ) {
506         next if defined($new_data{$key}) xor defined($current_data->{$key});
507         if ( !defined($new_data{$key}) || $current_data->{$key} eq $new_data{$key} || $hidden_fields->{$key} ) {
508            delete $new_data{$key};
509         }
510     }
511
512     return %new_data;
513 }
514
515 sub DelEmptyFields {
516     my (%borrower) = @_;
517
518     foreach my $key ( keys %borrower ) {
519         delete $borrower{$key} unless $borrower{$key};
520     }
521
522     return %borrower;
523 }
524
525 sub FilterUnchangedAttributes {
526     my ( $borrowernumber, $entered_attributes ) = @_;
527
528     my @patron_attributes = grep {$_->type->opac_editable ? $_ : ()} Koha::Patron::Attributes->search({ borrowernumber => $borrowernumber })->as_list;
529
530     my $patron_attribute_types;
531     foreach my $attr (@patron_attributes) {
532         $patron_attribute_types->{ $attr->code } += 1;
533     }
534
535     my $passed_attribute_types;
536     foreach my $attr (@{ $entered_attributes }) {
537         $passed_attribute_types->{ $attr->{ code } } += 1;
538     }
539
540     my @changed_attributes;
541
542     # Loop through the current patron attributes
543     foreach my $attribute_type ( keys %{ $patron_attribute_types } ) {
544         if ( $patron_attribute_types->{ $attribute_type } !=  $passed_attribute_types->{ $attribute_type } ) {
545             # count differs, overwrite all attributes for given type
546             foreach my $attr (@{ $entered_attributes }) {
547                 push @changed_attributes, $attr
548                     if $attr->{ code } eq $attribute_type;
549             }
550         } else {
551             # count matches, check values
552             my $changes = 0;
553             foreach my $attr (grep { $_->code eq $attribute_type } @patron_attributes) {
554                 $changes = 1
555                     unless any { $_->{ value } eq $attr->attribute } @{ $entered_attributes };
556                 last if $changes;
557             }
558
559             if ( $changes ) {
560                 foreach my $attr (@{ $entered_attributes }) {
561                     push @changed_attributes, $attr
562                         if $attr->{ code } eq $attribute_type;
563                 }
564             }
565         }
566     }
567
568     # Loop through passed attributes, looking for new ones
569     foreach my $attribute_type ( keys %{ $passed_attribute_types } ) {
570         if ( !defined $patron_attribute_types->{ $attribute_type } ) {
571             # YAY, new stuff
572             foreach my $attr (grep { $_->{code} eq $attribute_type } @{ $entered_attributes }) {
573                 push @changed_attributes, $attr;
574             }
575         }
576     }
577
578     return \@changed_attributes;
579 }
580
581 sub GeneratePatronAttributesForm {
582     my ( $borrowernumber, $entered_attributes ) = @_;
583
584     # Get all attribute types and the values for this patron (if applicable)
585     my @types = grep { $_->opac_editable() or $_->opac_display }
586         Koha::Patron::Attribute::Types->search()->as_list();
587     if ( scalar(@types) == 0 ) {
588         return [];
589     }
590
591     my @displayable_attributes = grep { $_->type->opac_display ? $_ : () }
592         Koha::Patron::Attributes->search({ borrowernumber => $borrowernumber })->as_list;
593
594     my %attr_values = ();
595
596     # Build the attribute values list either from the passed values
597     # or taken from the patron itself
598     if ( defined $entered_attributes ) {
599         foreach my $attr (@$entered_attributes) {
600             push @{ $attr_values{ $attr->{code} } }, $attr->{value};
601         }
602     }
603     elsif ( defined $borrowernumber ) {
604         my @editable_attributes = grep { $_->type->opac_editable ? $_ : () } @displayable_attributes;
605         foreach my $attr (@editable_attributes) {
606             push @{ $attr_values{ $attr->code } }, $attr->attribute;
607         }
608     }
609
610     # Add the non-editable attributes (that don't come from the form)
611     foreach my $attr ( grep { !$_->type->opac_editable } @displayable_attributes ) {
612         push @{ $attr_values{ $attr->code } }, $attr->attribute;
613     }
614
615     # Find all existing classes
616     my @classes = sort( uniq( map { $_->class } @types ) );
617     my %items_by_class;
618
619     foreach my $attr_type (@types) {
620         push @{ $items_by_class{ $attr_type->class() } }, {
621             type => $attr_type,
622             # If editable, make sure there's at least one empty entry,
623             # to make the template's job easier
624             values => $attr_values{ $attr_type->code() } || ['']
625         }
626             unless !defined $attr_values{ $attr_type->code() }
627                     and !$attr_type->opac_editable;
628     }
629
630     # Finally, build a list of containing classes
631     my @class_loop;
632     foreach my $class (@classes) {
633         next unless ( $items_by_class{$class} );
634
635         my $av = Koha::AuthorisedValues->search(
636             { category => 'PA_CLASS', authorised_value => $class } );
637
638         my $lib = $av->count ? $av->next->opac_description : $class;
639
640         push @class_loop,
641             {
642             class => $class,
643             items => $items_by_class{$class},
644             lib   => $lib,
645             };
646     }
647
648     return \@class_loop;
649 }
650
651 sub ParsePatronAttributes {
652     my ( $borrowernumber, $cgi ) = @_;
653
654     my @codes  = $cgi->multi_param('patron_attribute_code');
655     my @values = $cgi->multi_param('patron_attribute_value');
656
657     my @editable_attribute_types
658         = map { $_->code } Koha::Patron::Attribute::Types->search({ opac_editable => 1 });
659
660     my $ea = each_array( @codes, @values );
661     my @attributes;
662
663     my $delete_candidates = {};
664
665     while ( my ( $code, $value ) = $ea->() ) {
666         if ( any { $_ eq $code } @editable_attribute_types ) {
667             # It is an editable attribute
668             if ( !defined($value) or $value eq '' ) {
669                 $delete_candidates->{$code} = 1
670                     unless $delete_candidates->{$code};
671             }
672             else {
673                 # we've got a value
674                 push @attributes, { code => $code, attribute => $value };
675
676                 # 'code' is no longer a delete candidate
677                 delete $delete_candidates->{$code}
678                     if defined $delete_candidates->{$code};
679             }
680         }
681     }
682
683     foreach my $code ( keys %{$delete_candidates} ) {
684         if ( Koha::Patron::Attributes->search({
685                 borrowernumber => $borrowernumber, code => $code })->count > 0 )
686         {
687             push @attributes, { code => $code, attribute => '' }
688                 unless any { $_->{code} eq $code } @attributes;
689         }
690     }
691
692     return \@attributes;
693 }
694
695
696 1;