Bug 20287: Add plain_text_password (& Remove AddMember_Opac)
[koha.git] / Koha / Patron.pm
index ed4a1b3..fa0b53a 100644 (file)
@@ -22,6 +22,8 @@ use Modern::Perl;
 
 use Carp;
 use List::MoreUtils qw( uniq );
+use Module::Load::Conditional qw( can_load );
+use Text::Unaccent qw( unac_string );
 
 use C4::Context;
 use C4::Log;
@@ -38,9 +40,44 @@ use Koha::Patrons;
 use Koha::Virtualshelves;
 use Koha::Club::Enrollments;
 use Koha::Account;
+use Koha::Subscription::Routinglists;
+
+if ( ! can_load( modules => { 'Koha::NorwegianPatronDB' => undef } ) ) {
+   warn "Unable to load Koha::NorwegianPatronDB";
+}
 
 use base qw(Koha::Object);
 
+our $RESULTSET_PATRON_ID_MAPPING = {
+    Accountline          => 'borrowernumber',
+    Aqbasketuser         => 'borrowernumber',
+    Aqbudget             => 'budget_owner_id',
+    Aqbudgetborrower     => 'borrowernumber',
+    ArticleRequest       => 'borrowernumber',
+    BorrowerAttribute    => 'borrowernumber',
+    BorrowerDebarment    => 'borrowernumber',
+    BorrowerFile         => 'borrowernumber',
+    BorrowerModification => 'borrowernumber',
+    ClubEnrollment       => 'borrowernumber',
+    Issue                => 'borrowernumber',
+    ItemsLastBorrower    => 'borrowernumber',
+    Linktracker          => 'borrowernumber',
+    Message              => 'borrowernumber',
+    MessageQueue         => 'borrowernumber',
+    OldIssue             => 'borrowernumber',
+    OldReserve           => 'borrowernumber',
+    Rating               => 'borrowernumber',
+    Reserve              => 'borrowernumber',
+    Review               => 'borrowernumber',
+    SearchHistory        => 'userid',
+    Statistic            => 'borrowernumber',
+    Suggestion           => 'suggestedby',
+    TagAll               => 'borrowernumber',
+    Virtualshelfcontent  => 'borrowernumber',
+    Virtualshelfshare    => 'borrowernumber',
+    Virtualshelve        => 'owner',
+};
+
 =head1 NAME
 
 Koha::Patron - Koha Patron Object class
@@ -51,6 +88,163 @@ Koha::Patron - Koha Patron Object class
 
 =cut
 
+=head3 new
+
+=cut
+
+sub new {
+    my ( $class, $params ) = @_;
+
+    return $class->SUPER::new($params);
+}
+
+sub fixup_cardnumber {
+    my ( $self ) = @_;
+    my $max = Koha::Patrons->search({
+        cardnumber => {-regexp => '^-?[0-9]+$'}
+    }, {
+        select => \'CAST(cardnumber AS SIGNED)',
+        as => ['cast_cardnumber']
+    })->_resultset->get_column('cast_cardnumber')->max;
+    $self->cardnumber(($max || 0) +1);
+}
+
+# trim whitespace from data which has some non-whitespace in it.
+# Could be moved to Koha::Object if need to be reused
+sub trim_whitespaces {
+    my( $self ) = @_;
+
+    my $schema  = Koha::Database->new->schema;
+    my @columns = $schema->source($self->_type)->columns;
+
+    for my $column( @columns ) {
+        my $value = $self->$column;
+        if ( defined $value ) {
+            $value =~ s/^\s*|\s*$//g;
+            $self->$column($value);
+        }
+    }
+    return $self;
+}
+
+sub plain_text_password {
+    my ( $self, $password ) = @_;
+    if ( $password ) {
+        $self->{_plain_text_password} = $password;
+        return $self;
+    }
+    return $self->{_plain_text_password}
+        if $self->{_plain_text_password};
+
+    return;
+}
+
+sub store {
+    my ($self) = @_;
+
+    $self->_result->result_source->schema->txn_do(
+        sub {
+            if (
+                C4::Context->preference("autoMemberNum")
+                and ( not defined $self->cardnumber
+                    or $self->cardnumber eq '' )
+              )
+            {
+                # Warning: The caller is responsible for locking the members table in write
+                # mode, to avoid database corruption.
+                # We are in a transaction but the table is not locked
+                $self->fixup_cardnumber;
+            }
+            unless ( $self->in_storage ) {    #AddMember
+
+                unless( $self->category->in_storage ) {
+                    Koha::Exceptions::Object::FKConstraint->throw(
+                        broken_fk => 'categorycode',
+                        value     => $self->categorycode,
+                    );
+                }
+
+                $self->trim_whitespaces;
+
+                # Generate a valid userid/login if needed
+                $self->userid($self->generate_userid)
+                  if not $self->userid or not $self->has_valid_userid;
+
+                # Add expiration date if it isn't already there
+                unless ( $self->dateexpiry ) {
+                    $self->dateexpiry( $self->category->get_expiry_date );
+                }
+
+                # Add enrollment date if it isn't already there
+                unless ( $self->dateenrolled ) {
+                    $self->dateenrolled(dt_from_string);
+                }
+
+                # Set the privacy depending on the patron's category
+                my $default_privacy = $self->category->default_privacy || q{};
+                $default_privacy =
+                    $default_privacy eq 'default' ? 1
+                  : $default_privacy eq 'never'   ? 2
+                  : $default_privacy eq 'forever' ? 0
+                  :                                                   undef;
+                $self->privacy($default_privacy);
+
+                unless ( defined $self->privacy_guarantor_checkouts ) {
+                    $self->privacy_guarantor_checkouts(0);
+                }
+
+                # Make a copy of the plain text password for later use
+                $self->plain_text_password( $self->password );
+
+                # Create a disabled account if no password provided
+                $self->password( $self->password
+                    ? Koha::AuthUtils::hash_password( $self->password )
+                    : '!' );
+
+                # We don't want invalid dates in the db (mysql has a bad habit of inserting 0000-00-00)
+                $self->dateofbirth(undef) unless $self->dateofbirth;
+                $self->debarred(undef)    unless $self->debarred;
+
+                # Set default values if not set
+                $self->sms_provider_id(undef) unless $self->sms_provider_id;
+                $self->guarantorid(undef)     unless $self->guarantorid;
+
+                $self->borrowernumber(undef);
+
+                $self = $self->SUPER::store;
+
+                # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
+                # cronjob will use for syncing with NL
+                if (   C4::Context->preference('NorwegianPatronDBEnable')
+                    && C4::Context->preference('NorwegianPatronDBEnable') == 1 )
+                {
+                    Koha::Database->new->schema->resultset('BorrowerSync')
+                      ->create(
+                        {
+                            'borrowernumber' => $self->borrowernumber,
+                            'synctype'       => 'norwegianpatrondb',
+                            'sync'           => 1,
+                            'syncstatus'     => 'new',
+                            'hashed_pin' =>
+                              Koha::NorwegianPatronDB::NLEncryptPIN($self->plain_text_password),
+                        }
+                      );
+                }
+
+                $self->add_enrolment_fee_if_needed;
+
+                logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
+                  if C4::Context->preference("BorrowersLog");
+            }
+            else {    #ModMember
+                $self = $self->SUPER::store;
+            }
+
+        }
+    );
+    return $self;
+}
+
 =head3 delete
 
 $patron->delete
@@ -200,6 +394,54 @@ sub siblings {
     );
 }
 
+=head3 merge_with
+
+    my $patron = Koha::Patrons->find($id);
+    $patron->merge_with( \@patron_ids );
+
+    This subroutine merges a list of patrons into the patron record. This is accomplished by finding
+    all related patron ids for the patrons to be merged in other tables and changing the ids to be that
+    of the keeper patron.
+
+=cut
+
+sub merge_with {
+    my ( $self, $patron_ids ) = @_;
+
+    my @patron_ids = @{ $patron_ids };
+
+    # Ensure the keeper isn't in the list of patrons to merge
+    @patron_ids = grep { $_ ne $self->id } @patron_ids;
+
+    my $schema = Koha::Database->new()->schema();
+
+    my $results;
+
+    $self->_result->result_source->schema->txn_do( sub {
+        foreach my $patron_id (@patron_ids) {
+            my $patron = Koha::Patrons->find( $patron_id );
+
+            next unless $patron;
+
+            # Unbless for safety, the patron will end up being deleted
+            $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
+
+            while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
+                my $rs = $schema->resultset($r)->search({ $field => $patron_id });
+                $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
+                $rs->update({ $field => $self->id });
+            }
+
+            $patron->move_to_deleted();
+            $patron->delete();
+        }
+    });
+
+    return $results;
+}
+
+
+
 =head3 wants_check_for_previous_checkout
 
     $wants_check = $patron->wants_check_for_previous_checkout;
@@ -297,7 +539,7 @@ Returns 1 if the patron is expired or 0;
 sub is_expired {
     my ($self) = @_;
     return 0 unless $self->dateexpiry;
-    return 0 if $self->dateexpiry eq '0000-00-00';
+    return 0 if $self->dateexpiry =~ '^9999';
     return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
     return 0;
 }
@@ -317,7 +559,7 @@ sub is_going_to_expire {
 
     return 0 unless $delay;
     return 0 unless $self->dateexpiry;
-    return 0 if $self->dateexpiry eq '0000-00-00';
+    return 0 if $self->dateexpiry =~ '^9999';
     return 1 if dt_from_string( $self->dateexpiry )->subtract( days => $delay ) < dt_from_string->truncate( to => 'day' );
     return 0;
 }
@@ -523,6 +765,37 @@ sub checkouts {
     return Koha::Checkouts->_new_from_dbic( $checkouts );
 }
 
+=head3 pending_checkouts
+
+my $pending_checkouts = $patron->pending_checkouts
+
+This method will return the same as $self->checkouts, but with a prefetch on
+items, biblio and biblioitems.
+
+It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
+
+It should not be used directly, prefer to access fields you need instead of
+retrieving all these fields in one go.
+
+
+=cut
+
+sub pending_checkouts {
+    my( $self ) = @_;
+    my $checkouts = $self->_result->issues->search(
+        {},
+        {
+            order_by => [
+                { -desc => 'me.timestamp' },
+                { -desc => 'issuedate' },
+                { -desc => 'issue_id' }, # Sort by issue_id should be enough
+            ],
+            prefetch => { item => { biblio => 'biblioitems' } },
+        }
+    );
+    return Koha::Checkouts->_new_from_dbic( $checkouts );
+}
+
 =head3 old_checkouts
 
 my $old_checkouts = $patron->old_checkouts
@@ -539,7 +812,7 @@ sub old_checkouts {
 
 my $overdue_items = $patron->get_overdues
 
-Return the overdued items
+Return the overdue items
 
 =cut
 
@@ -556,6 +829,20 @@ sub get_overdues {
     );
 }
 
+=head3 get_routing_lists
+
+my @routinglists = $patron->get_routing_lists
+
+Returns the routing lists a patron is subscribed to.
+
+=cut
+
+sub get_routing_lists {
+    my ($self) = @_;
+    my $routing_list_rs = $self->_result->subscriptionroutinglists;
+    return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
+}
+
 =head3 get_age
 
 my $age = $patron->get_age
@@ -831,7 +1118,85 @@ sub is_child {
     return $self->category->category_type eq 'C' ? 1 : 0;
 }
 
-=head3 type
+=head3 has_valid_userid
+
+my $patron = Koha::Patrons->find(42);
+$patron->userid( $new_userid );
+my $has_a_valid_userid = $patron->has_valid_userid
+
+my $patron = Koha::Patron->new( $params );
+my $has_a_valid_userid = $patron->has_valid_userid
+
+Return true if the current userid of this patron is valid/unique, otherwise false.
+
+Note that this should be done in $self->store instead and raise an exception if needed.
+
+=cut
+
+sub has_valid_userid {
+    my ($self) = @_;
+
+    return 0 unless $self->userid;
+
+    return 0 if ( $self->userid eq C4::Context->config('user') );    # DB user
+
+    my $already_exists = Koha::Patrons->search(
+        {
+            userid => $self->userid,
+            (
+                $self->in_storage
+                ? ( borrowernumber => { '!=' => $self->borrowernumber } )
+                : ()
+            ),
+        }
+    )->count;
+    return $already_exists ? 0 : 1;
+}
+
+=head3 generate_userid
+
+my $patron = Koha::Patron->new( $params );
+my $userid = $patron->generate_userid
+
+Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
+
+Return the generate userid ($firstname.$surname if there is a $firstname, or $surname if there is no value in $firstname) plus offset (0 if the $userid is unique, or a higher numeric value if not unique).
+
+# Note: Should we set $self->userid with the generated value?
+# Certainly yes, but we AddMember and ModMember will be rewritten
+
+=cut
+
+sub generate_userid {
+    my ($self) = @_;
+    my $userid;
+    my $offset = 0;
+    my $existing_userid = $self->userid;
+    my $firstname = $self->firstname // q{};
+    my $surname = $self->surname // q{};
+    #The script will "do" the following code and increment the $offset until the generated userid is unique
+    do {
+      $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
+      $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
+      $userid = lc(($firstname)? "$firstname.$surname" : $surname);
+      $userid = unac_string('utf-8',$userid);
+      $userid .= $offset unless $offset == 0;
+      $self->userid( $userid );
+      $offset++;
+     } while (! $self->has_valid_userid );
+
+     # Resetting to the previous value as the callers do not expect
+     # this method to modify the userid attribute
+     # This will be done later (move of AddMember and ModMember)
+     $self->userid( $existing_userid );
+
+     return $userid;
+
+}
+
+=head2 Internal methods
+
+=head3 _type
 
 =cut