Bug 33159: Correctly form thesaurus term for non-subject headings
[koha-ffzg.git] / C4 / Auth_with_ldap.pm
index a58f4e6..f13dbdc 100644 (file)
@@ -17,29 +17,23 @@ package C4::Auth_with_ldap;
 # You should have received a copy of the GNU General Public License
 # along with Koha; if not, see <http://www.gnu.org/licenses>.
 
-use strict;
-#use warnings; FIXME - Bug 2505
-use Carp;
+use Modern::Perl;
+use Carp qw( croak );
 
-use C4::Debug;
 use C4::Context;
-use C4::Members qw(AddMember);
-use C4::Members::Attributes;
-use C4::Members::AttributeTypes;
 use C4::Members::Messaging;
-use C4::Auth qw(checkpw_internal);
+use C4::Auth qw( checkpw_internal );
+use C4::Letters qw( GetPreparedLetter EnqueueLetter SendQueuedMessages );
 use Koha::Patrons;
-use Koha::AuthUtils qw(hash_password);
-use List::MoreUtils qw( any );
+use Koha::AuthUtils qw( hash_password );
 use Net::LDAP;
 use Net::LDAP::Filter;
 
-use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug);
-
+our (@ISA, @EXPORT_OK);
 BEGIN {
        require Exporter;
        @ISA    = qw(Exporter);
-       @EXPORT = qw( checkpw_ldap );
+       @EXPORT_OK = qw( checkpw_ldap );
 }
 
 # Redefine checkpw_ldap:
@@ -54,17 +48,20 @@ sub ldapserver_error {
 }
 
 use vars qw($mapping @ldaphosts $base $ldapname $ldappassword);
-my $context = C4::Context->new()       or die 'C4::Context->new failed';
 my $ldap = C4::Context->config("ldapserver") or die 'No "ldapserver" in server hash from KOHA_CONF: ' . $ENV{KOHA_CONF};
+# since Bug 28278 we need to skip id in <ldapserver id="ldapserver"> which generates additional hash level
+if ( exists $ldap->{ldapserver} ) {
+    $ldap = $ldap->{ldapserver}         or die ldapserver_error('id="ldapserver"');
+}
 my $prefhost  = $ldap->{hostname}      or die ldapserver_error('hostname');
 my $base      = $ldap->{base}          or die ldapserver_error('base');
 $ldapname     = $ldap->{user}          ;
 $ldappassword = $ldap->{pass}          ;
 our %mapping  = %{$ldap->{mapping}}; # FIXME dpavlin -- don't die because of || (); from 6eaf8511c70eb82d797c941ef528f4310a15e9f9
 my @mapkeys = keys %mapping;
-$debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (  total  ): ", join ' ', @mapkeys, "\n";
+#warn "Got ", scalar(@mapkeys), " ldap mapkeys (  total  ): ", join ' ', @mapkeys, "\n";
 @mapkeys = grep {defined $mapping{$_}->{is}} @mapkeys;
-$debug and print STDERR "Got ", scalar(@mapkeys), " ldap mapkeys (populated): ", join ' ', @mapkeys, "\n";
+#warn "Got ", scalar(@mapkeys), " ldap mapkeys (populated): ", join ' ', @mapkeys, "\n";
 
 my %categorycode_conversions;
 my $default_categorycode;
@@ -76,9 +73,10 @@ if(defined $ldap->{categorycode_mapping}) {
 }
 
 my %config = (
-       anonymous => ($ldapname and $ldappassword) ? 0 : 1,
+    anonymous => defined ($ldap->{anonymous_bind}) ? $ldap->{anonymous_bind} : 1,
     replicate => defined($ldap->{replicate}) ? $ldap->{replicate} : 1,  #    add from LDAP to Koha database for new user
-       update => defined($ldap->{update}   ) ? $ldap->{update}    : 1,  # update from LDAP to Koha database for existing user
+    welcome   => defined($ldap->{welcome}) ? $ldap->{welcome} : 0,  #    send welcome notice when patron is added via replicate
+    update    => defined($ldap->{update}) ? $ldap->{update} : 1,  # update from LDAP to Koha database for existing user
 );
 
 sub description {
@@ -105,15 +103,15 @@ sub search_method {
                warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count) . description($search);
                return 0;
        }
-       if ($count != 1) {
-               warn sprintf("LDAP Auth rejected : %s gets %d hits\n", $filter->as_string, $count);
-               return 0;
-       }
+    if ($count == 0) {
+        warn sprintf("LDAP Auth rejected : search with filter '%s' returns no hit\n", $filter->as_string);
+        return 0;
+    }
     return $search;
 }
 
 sub checkpw_ldap {
-    my ($dbh, $userid, $password) = @_;
+    my ($userid, $password) = @_;
     my @hosts = split(',', $prefhost);
     my $db = Net::LDAP->new(\@hosts);
     unless ( $db ) {
@@ -121,12 +119,12 @@ sub checkpw_ldap {
         return 0;
     }
 
-       #$debug and $db->debug(5);
     my $userldapentry;
 
+    # first, LDAP authentication
     if ( $ldap->{auth_by_bind} ) {
         my $principal_name;
-        if ( $ldap->{anonymous_bind} ) {
+        if ( $config{anonymous} ) {
 
             # Perform an anonymous bind
             my $res = $db->bind;
@@ -154,7 +152,7 @@ sub checkpw_ldap {
         # Perform a LDAP bind for the given username using the matched DN
         my $res = $db->bind( $principal_name, password => $password );
         if ( $res->code ) {
-            if ( $ldap->{anonymous_bind} ) {
+            if ( $config{anonymous} ) {
                 # With anonymous_bind approach we can be sure we have found the correct user
                 # and that any 'code' response indicates a 'bad' user (be that blocked, banned
                 # or password changed). We should not fall back to local accounts in this case.
@@ -175,19 +173,30 @@ sub checkpw_ldap {
             $userldapentry = $search->shift_entry;
         }
     } else {
-               my $res = ($config{anonymous}) ? $db->bind : $db->bind($ldapname, password=>$ldappassword);
+        my $res = ($config{anonymous}) ? $db->bind : $db->bind($ldapname, password=>$ldappassword);
                if ($res->code) {               # connection refused
                        warn "LDAP bind failed as ldapuser " . ($ldapname || '[ANONYMOUS]') . ": " . description($res);
                        return 0;
                }
         my $search = search_method($db, $userid) or return 0;   # warnings are in the sub
-        $userldapentry = $search->shift_entry;
-               my $cmpmesg = $db->compare( $userldapentry, attr=>'userpassword', value => $password );
-               if ($cmpmesg->code != 6) {
-                       warn "LDAP Auth rejected : invalid password for user '$userid'. " . description($cmpmesg);
-                       return -1;
-               }
-       }
+        # Handle multiple branches. Same login exists several times in different branches.
+        my $bind_ok = 0;
+        while (my $entry = $search->shift_entry) {
+            my $user_ldap_bind_ret = $db->bind($entry->dn, password => $password);
+            unless ($user_ldap_bind_ret->code) {
+                $userldapentry = $entry;
+                $bind_ok = 1;
+                last;
+            }
+        }
+
+        unless ($bind_ok) {
+            warn "LDAP Auth rejected : invalid password for user '$userid'.";
+            return -1;
+        }
+
+
+    }
 
     # To get here, LDAP has accepted our user's login attempt.
     # But we still have work to do.  See perldoc below for detailed breakdown.
@@ -198,7 +207,7 @@ sub checkpw_ldap {
     if (( $borrowernumber and $config{update}   ) or
         (!$borrowernumber and $config{replicate})   ) {
         %borrower = ldap_entry_2_hash($userldapentry,$userid);
-        $debug and print STDERR "checkpw_ldap received \%borrower w/ " . keys(%borrower), " keys: ", join(' ', keys %borrower), "\n";
+        #warn "checkpw_ldap received \%borrower w/ " . keys(%borrower), " keys: ", join(' ', keys %borrower), "\n";
     }
 
     if ($borrowernumber) {
@@ -210,21 +219,71 @@ sub checkpw_ldap {
                return(1, $cardnumber, $local_userid);
         }
     } elsif ($config{replicate}) { # A2, C2
-        $borrowernumber = AddMember(%borrower) or die "AddMember failed";
-        C4::Members::Messaging::SetMessagingPreferencesFromDefaults( { borrowernumber => $borrowernumber, categorycode => $borrower{'categorycode'} } );
+        my @columns = Koha::Patrons->columns;
+        my $patron = Koha::Patron->new(
+            {
+                map { exists( $borrower{$_} ) ? ( $_ => $borrower{$_} ) : () } @columns
+            }
+        )->store;
+        die "Insert of new patron failed" unless $patron;
+        $borrowernumber = $patron->borrowernumber;
+        C4::Members::Messaging::SetMessagingPreferencesFromDefaults(
+            {
+                borrowernumber => $borrowernumber,
+                categorycode   => $borrower{'categorycode'}
+            }
+        );
+
+        # Send welcome email if enabled
+        if ( $config{welcome} ) {
+            my $emailaddr = $patron->notice_email_address;
+
+            # if we manage to find a valid email address, send notice
+            if ($emailaddr) {
+                my $letter = C4::Letters::GetPreparedLetter(
+                    module      => 'members',
+                    letter_code => 'WELCOME',
+                    branchcode  => $patron->branchcode,
+                    lang        => $patron->lang || 'default',
+                    tables      => {
+                        'branches'  => $patron->branchcode,
+                        'borrowers' => $patron->borrowernumber,
+                    },
+                    want_librarian => 1,
+                ) or return;
+
+                my $message_id = C4::Letters::EnqueueLetter(
+                    {
+                        letter                 => $letter,
+                        borrowernumber         => $patron->id,
+                        to_address             => $emailaddr,
+                        message_transport_type => 'email'
+                    }
+                );
+
+                C4::Letters::SendQueuedMessages( { message_id => $message_id } );
+            }
+        }
    } else {
         return 0;   # B2, D2
     }
     if (C4::Context->preference('ExtendedPatronAttributes') && $borrowernumber && ($config{update} ||$config{replicate})) {
-        foreach my $attribute_type ( C4::Members::AttributeTypes::GetAttributeTypes() ) {
-            my $code = $attribute_type->{code};
+        my $library_id = C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
+        my $attribute_types = Koha::Patron::Attribute::Types->search_with_library_limits({}, {}, $library_id);
+        while ( my $attribute_type = $attribute_types->next ) {
+            my $code = $attribute_type->code;
             unless (exists($borrower{$code}) && $borrower{$code} !~ m/^\s*$/ ) {
                 next;
             }
-            if (C4::Members::Attributes::CheckUniqueness($code, $borrower{$code}, $borrowernumber)) {
-                C4::Members::Attributes::UpdateBorrowerAttribute($borrowernumber, {code => $code, attribute => $borrower{$code}});
-            } else {
-                warn "ERROR_extended_unique_id_failed $code $borrower{$code}";
+            my $patron = Koha::Patrons->find($borrowernumber);
+            if ( $patron ) { # Should not be needed, but we are in C4::Auth LDAP...
+                eval {
+                    my $attribute = Koha::Patron::Attribute->new({code => $code, attribute => $borrower{$code}});
+                    $patron->extended_attributes([$attribute->unblessed]);
+                };
+                if ($@) { # FIXME Test if Koha::Exceptions::Patron::Attribute::NonRepeatable
+                    warn "ERROR_extended_unique_id_failed $code $borrower{$code}";
+                }
             }
         }
     }
@@ -241,25 +300,22 @@ sub ldap_entry_2_hash {
        my %borrower = ( cardnumber => shift );
        my %memberhash;
        $userldapentry->exists('uid');  # This is bad, but required!  By side-effect, this initializes the attrs hash. 
-       if ($debug) {
-               foreach (keys %$userldapentry) {
-                       print STDERR "\n\nLDAP key: $_\t", sprintf('(%s)', ref $userldapentry->{$_}), "\n";
-               }
-       }
+    #foreach (keys %$userldapentry) {
+    #    print STDERR "\n\nLDAP key: $_\t", sprintf('(%s)', ref $userldapentry->{$_}), "\n";
+    #}
        my $x = $userldapentry->{attrs} or return;
        foreach (keys %$x) {
                $memberhash{$_} = join ' ', @{$x->{$_}};        
-               $debug and print STDERR sprintf("building \$memberhash{%s} = ", $_, join(' ', @{$x->{$_}})), "\n";
+        #warn sprintf("building \$memberhash{%s} = ", $_, join(' ', @{$x->{$_}})), "\n";
        }
-       $debug and print STDERR "Finsihed \%memberhash has ", scalar(keys %memberhash), " keys\n",
-                                       "Referencing \%mapping with ", scalar(keys %mapping), " keys\n";
+    #warn "Finished \%memberhash has ", scalar(keys %memberhash), " keys\n", "Referencing \%mapping with ", scalar(keys %mapping), " keys\n";
        foreach my $key (keys %mapping) {
                my  $data = $memberhash{ lc($mapping{$key}->{is}) }; # Net::LDAP returns all names in lowercase
-               $debug and printf STDERR "mapping %20s ==> %-20s (%s)\n", $key, $mapping{$key}->{is}, $data;
+        #warn "mapping %20s ==> %-20s (%s)\n", $key, $mapping{$key}->{is}, $data;
                unless (defined $data) { 
-                       $data = $mapping{$key}->{content} || '';        # default or failsafe ''
+            $data = $mapping{$key}->{content} || undef;
                }
-               $borrower{$key} = ($data ne '') ? $data : ' ' ;
+        $borrower{$key} = $data;
        }
        $borrower{initials} = $memberhash{initials} || 
                ( substr($borrower{'firstname'},0,1)
@@ -280,7 +336,7 @@ sub ldap_entry_2_hash {
        $sth->execute( uc($borrower{'categorycode'}) );
        unless ( my $row = $sth->fetchrow_hashref ) {
                my $default = $mapping{'categorycode'}->{content};
-               $debug && warn "Can't find ", $borrower{'categorycode'}, " default to: $default for ", $borrower{userid};
+        #warn "Can't find ", $borrower{'categorycode'}, " default to: $default for ", $borrower{userid};
                $borrower{'categorycode'} = $default
        }
 
@@ -294,12 +350,12 @@ sub exists_local {
 
        my $sth = $dbh->prepare("$select WHERE userid=?");      # was cardnumber=?
        $sth->execute($arg);
-       $debug and printf STDERR "Userid '$arg' exists_local? %s\n", $sth->rows;
+    #warn "Userid '$arg' exists_local? %s\n", $sth->rows;
        ($sth->rows == 1) and return $sth->fetchrow;
 
        $sth = $dbh->prepare("$select WHERE cardnumber=?");
        $sth->execute($arg);
-       $debug and printf STDERR "Cardnumber '$arg' exists_local? %s\n", $sth->rows;
+    #warn "Cardnumber '$arg' exists_local? %s\n", $sth->rows;
        ($sth->rows == 1) and return $sth->fetchrow;
        return 0;
 }
@@ -332,12 +388,12 @@ sub _do_changepassword {
         $sth->execute($borrowerid);
         return $cardnum;
     }
-    my $digest = hash_password($password);
 
-    $debug and print STDERR "changing local password for borrowernumber=$borrowerid to '$digest'\n";
-    Koha::Patrons->find($borrowerid)->update_password( $userid, $digest );
+    my $digest = hash_password($password);
+    #warn "changing local password for borrowernumber=$borrowerid to '$digest'\n";
+    Koha::Patrons->find($borrowerid)->set_password({ password => $password, skip_validation => 1 });
 
-    my ($ok, $cardnum) = checkpw_internal(C4::Context->dbh, $userid, $password);
+    my ($ok, $cardnum) = checkpw_internal($userid, $password);
     return $cardnum if $ok;
 
     warn "Password mismatch after update to borrowernumber=$borrowerid";
@@ -350,23 +406,31 @@ sub update_local {
     my $borrowerid = shift or croak "No borrowerid";
     my $borrower   = shift or croak "No borrower record";
 
+    # skip extended patron attributes in 'borrowers' attribute update
     my @keys = keys %$borrower;
+    if (C4::Context->preference('ExtendedPatronAttributes')) {
+        my $library_id = C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
+        my $attribute_types = Koha::Patron::Attribute::Types->search_with_library_limits({}, {}, $library_id);
+        while ( my $attribute_type = $attribute_types->next ) {
+           my $code = $attribute_type->code;
+           @keys = grep { $_ ne $code } @keys;
+           #warn "ignoring extended patron attribute '%s' in update_local()\n", $code;
+        }
+    }
+
     my $dbh = C4::Context->dbh;
     my $query = "UPDATE  borrowers\nSET     " .
         join(',', map {"$_=?"} @keys) .
         "\nWHERE   borrowernumber=? ";
     my $sth = $dbh->prepare($query);
-    if ($debug) {
-        print STDERR $query, "\n",
-            join "\n", map {"$_ = '" . $borrower->{$_} . "'"} @keys;
-        print STDERR "\nuserid = $userid\n";
-    }
+    #warn $query, "\n", join "\n", map {"$_ = '" . $borrower->{$_} . "'"} @keys;
+    #warn "\nuserid = $userid\n";
     $sth->execute(
         ((map {$borrower->{$_}} @keys), $borrowerid)
     );
 
     # MODIFY PASSWORD/LOGIN if password was mapped
-    _do_changepassword($userid, $borrowerid, $password) if $borrower->{'password'};
+    _do_changepassword($userid, $borrowerid, $password) if exists( $borrower->{'password'} );
 }
 
 1;
@@ -441,7 +505,6 @@ C4::Auth - Authenticates Koha users
                | contactname         | mediumtext   | YES  |     | NULL    |                |
                | contactfirstname    | text         | YES  |     | NULL    |                |
                | contacttitle        | text         | YES  |     | NULL    |                |
-               | guarantorid         | int(11)      | YES  | MUL | NULL    |                |
                | borrowernotes       | mediumtext   | YES  |     | NULL    |                |
                | relationship        | varchar(100) | YES  |     | NULL    |                |
                | ethnicity           | varchar(50)  | YES  |     | NULL    |                |
@@ -483,6 +546,7 @@ Example XML stanza for LDAP configuration in KOHA_CONF.
     <user>cn=Manager,dc=metavore,dc=com</user>             <!-- DN, if not anonymous -->
     <pass>metavore</pass>          <!-- password, if not anonymous -->
     <replicate>1</replicate>       <!-- add new users from LDAP to Koha database -->
+    <welcome>1</welcome>           <!-- send new users the welcome email when added via replicate -->
     <update>1</update>             <!-- update existing users in Koha database -->
     <auth_by_bind>0</auth_by_bind> <!-- set to 1 to authenticate by binding instead of
                                         password comparison, e.g., to use Active Directory -->