Bug 35918: Fix auto library connect (AutoLocation)
[koha.git] / C4 / Auth.pm
1 package C4::Auth;
2
3 # Copyright 2000-2002 Katipo Communications
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use strict;
21 use warnings;
22 use Carp qw( croak );
23
24 use Digest::MD5 qw( md5_base64 );
25 use CGI::Session;
26 use CGI::Session::ErrorHandler;
27 use URI;
28 use URI::QueryParam;
29 use List::MoreUtils qw( uniq );
30
31 use C4::Context;
32 use C4::Templates;    # to get the template
33 use C4::Languages;
34 use C4::Search::History;
35 use Koha;
36 use Koha::Logger;
37 use Koha::Caches;
38 use Koha::AuthUtils qw( get_script_name hash_password );
39 use Koha::Auth::TwoFactorAuth;
40 use Koha::Checkouts;
41 use Koha::DateUtils qw( dt_from_string );
42 use Koha::Library::Groups;
43 use Koha::Libraries;
44 use Koha::Cash::Registers;
45 use Koha::Desks;
46 use Koha::Patrons;
47 use Koha::Patron::Consents;
48 use List::MoreUtils qw( any );
49 use Encode;
50 use C4::Auth_with_shibboleth qw( shib_ok get_login_shib login_shib_url logout_shib checkpw_shib );
51 use Net::CIDR;
52 use C4::Log qw( logaction );
53 use Koha::CookieManager;
54 use Koha::Token;
55
56 # use utf8;
57
58 use vars qw($ldap $cas $caslogout);
59 our (@ISA, @EXPORT_OK);
60 BEGIN {
61     sub psgi_env { any { /^psgi\./ } keys %ENV }
62
63     sub safe_exit {
64         if   (psgi_env) { die 'psgi:exit' }
65         else            { exit }
66     }
67
68     C4::Context->set_remote_address;
69
70     require Exporter;
71     @ISA = qw(Exporter);
72
73     @EXPORT_OK = qw(
74       checkauth check_api_auth get_session check_cookie_auth checkpw checkpw_internal checkpw_hash
75       get_all_subpermissions get_user_subpermissions track_login_daily in_iprange
76       get_template_and_user haspermission
77     );
78
79     $ldap      = C4::Context->config('useldapserver') || 0;
80     $cas       = C4::Context->preference('casAuthentication');
81     $caslogout = C4::Context->preference('casLogout');
82
83     if ($ldap) {
84         require C4::Auth_with_ldap;
85         import C4::Auth_with_ldap qw(checkpw_ldap);
86     }
87     if ($cas) {
88         require C4::Auth_with_cas;    # no import
89         import C4::Auth_with_cas qw(check_api_auth_cas checkpw_cas login_cas logout_cas login_cas_url logout_if_required multipleAuth getMultipleAuth);
90     }
91
92 }
93
94 =head1 NAME
95
96 C4::Auth - Authenticates Koha users
97
98 =head1 SYNOPSIS
99
100   use CGI qw ( -utf8 );
101   use C4::Auth;
102   use C4::Output;
103
104   my $query = CGI->new;
105
106   my ($template, $borrowernumber, $cookie)
107     = get_template_and_user(
108         {
109             template_name   => "opac-main.tt",
110             query           => $query,
111       type            => "opac",
112       authnotrequired => 0,
113       flagsrequired   => { catalogue => '*', tools => 'import_patrons' },
114   }
115     );
116
117   output_html_with_http_headers $query, $cookie, $template->output;
118
119 =head1 DESCRIPTION
120
121 The main function of this module is to provide
122 authentification. However the get_template_and_user function has
123 been provided so that a users login information is passed along
124 automatically. This gets loaded into the template.
125
126 =head1 FUNCTIONS
127
128 =head2 get_template_and_user
129
130  my ($template, $borrowernumber, $cookie)
131      = get_template_and_user(
132        {
133          template_name   => "opac-main.tt",
134          query           => $query,
135          type            => "opac",
136          authnotrequired => 0,
137          flagsrequired   => { catalogue => '*', tools => 'import_patrons' },
138        }
139      );
140
141 This call passes the C<query>, C<flagsrequired> and C<authnotrequired>
142 to C<&checkauth> (in this module) to perform authentification.
143 See C<&checkauth> for an explanation of these parameters.
144
145 The C<template_name> is then used to find the correct template for
146 the page. The authenticated users details are loaded onto the
147 template in the logged_in_user variable (which is a Koha::Patron object). Also the
148 C<sessionID> is passed to the template. This can be used in templates
149 if cookies are disabled. It needs to be put as and input to every
150 authenticated page.
151
152 More information on the C<gettemplate> sub can be found in the
153 Output.pm module.
154
155 =cut
156
157 sub get_template_and_user {
158
159     my $in = shift;
160     my ( $user, $cookie, $sessionID, $flags );
161     $cookie = [];
162
163     my $cookie_mgr = Koha::CookieManager->new;
164
165     # Get shibboleth login attribute
166     my $shib = C4::Context->config('useshibboleth') && shib_ok();
167     my $shib_login = $shib ? get_login_shib() : undef;
168
169     C4::Context->interface( $in->{type} );
170
171     $in->{'authnotrequired'} ||= 0;
172
173     # the following call includes a bad template check; might croak
174     my $template = C4::Templates::gettemplate(
175         $in->{'template_name'},
176         $in->{'type'},
177         $in->{'query'},
178     );
179
180     if ( $in->{'template_name'} !~ m/maintenance/ ) {
181         ( $user, $cookie, $sessionID, $flags ) = checkauth(
182             $in->{'query'},
183             $in->{'authnotrequired'},
184             $in->{'flagsrequired'},
185             $in->{'type'},
186             undef,
187             $in->{template_name},
188         );
189     }
190
191     # If we enforce GDPR and the user did not consent, redirect
192     # Exceptions for consent page itself and SCI/SCO system
193     if( $in->{type} eq 'opac' && $user &&
194         $in->{'template_name'} !~ /^(opac-patron-consent|sc[io]\/)/ &&
195         C4::Context->preference('GDPR_Policy') eq 'Enforced' )
196     {
197         my $consent = Koha::Patron::Consents->search({
198             borrowernumber => getborrowernumber($user),
199             type => 'GDPR_PROCESSING',
200             given_on => { '!=', undef },
201         })->next;
202         if( !$consent ) {
203             print $in->{query}->redirect(-uri => '/cgi-bin/koha/opac-patron-consent.pl', -cookie => $cookie);
204             safe_exit;
205         }
206     }
207
208     if ( $in->{type} eq 'opac' && $user ) {
209         my $is_sco_user;
210         if ($sessionID){
211             my $session = get_session($sessionID);
212             if ($session){
213                 $is_sco_user = $session->param('sco_user');
214             }
215         }
216         my $kick_out;
217
218         if (
219 # If the user logged in is the SCO user and they try to go out of the SCO module,
220 # log the user out removing the CGISESSID cookie
221             $in->{template_name} !~ m|sco/| && $in->{template_name} !~ m|errors/errorpage.tt|
222             && (
223                 $is_sco_user ||
224                 (
225                     C4::Context->preference('AutoSelfCheckID')
226                     && $user eq C4::Context->preference('AutoSelfCheckID')
227                 )
228             )
229           )
230         {
231             $kick_out = 1;
232         }
233         elsif (
234 # If the user logged in is the SCI user and they try to go out of the SCI module,
235 # kick them out unless it is SCO with a valid permission
236 # or they are a superlibrarian
237                $in->{template_name} !~ m|sci/|
238             && haspermission( $user, { self_check => 'self_checkin_module' } )
239             && !(
240                 $in->{template_name} =~ m|sco/| && haspermission(
241                     $user, { self_check => 'self_checkout_module' }
242                 )
243             )
244             && $flags && $flags->{superlibrarian} != 1
245           )
246         {
247             $kick_out = 1;
248         }
249
250         if ($kick_out) {
251             $template = C4::Templates::gettemplate( 'opac-auth.tt', 'opac',
252                 $in->{query} );
253             $cookie = $cookie_mgr->replace_in_list( $cookie, $in->{query}->cookie(
254                 -name     => 'CGISESSID',
255                 -value    => '',
256                 -HttpOnly => 1,
257                 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
258                 -sameSite => 'Lax',
259             ));
260
261             $template->param(
262                 loginprompt => 1,
263                 script_name => get_script_name(),
264             );
265
266             print $in->{query}->header(
267                 {
268                     type              => 'text/html',
269                     charset           => 'utf-8',
270                     cookie            => $cookie,
271                     'X-Frame-Options' => 'SAMEORIGIN'
272                 }
273               ),
274               $template->output;
275             safe_exit;
276         }
277     }
278
279     my $borrowernumber;
280     if ($user) {
281
282         # It's possible for $user to be the borrowernumber if they don't have a
283         # userid defined (and are logging in through some other method, such
284         # as SSL certs against an email address)
285         my $patron;
286         $borrowernumber = getborrowernumber($user) if defined($user);
287         if ( !defined($borrowernumber) && defined($user) ) {
288             $patron = Koha::Patrons->find( $user );
289             if ($patron) {
290                 $borrowernumber = $user;
291
292                 # A bit of a hack, but I don't know there's a nicer way
293                 # to do it.
294                 $user = $patron->firstname . ' ' . $patron->surname;
295             }
296         } else {
297             $patron = Koha::Patrons->find( $borrowernumber );
298             # FIXME What to do if $patron does not exist?
299         }
300
301         # user info
302         $template->param( loggedinusername   => $user ); # OBSOLETE - Do not reuse this in template, use logged_in_user.userid instead
303         $template->param( loggedinusernumber => $borrowernumber ); # FIXME Should be replaced with logged_in_user.borrowernumber
304         $template->param( logged_in_user     => $patron );
305         $template->param( sessionID          => $sessionID );
306         $template->param( csrf_token => Koha::Token->new->generate_csrf({ session_id => scalar $sessionID }));
307
308         if ( $in->{'type'} eq 'opac' ) {
309             require Koha::Virtualshelves;
310             my $some_private_shelves = Koha::Virtualshelves->get_some_shelves(
311                 {
312                     borrowernumber => $borrowernumber,
313                     public         => 0,
314                 }
315             );
316             my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
317                 {
318                     public => 1,
319                 }
320             );
321             $template->param(
322                 some_private_shelves => $some_private_shelves,
323                 some_public_shelves  => $some_public_shelves,
324             );
325         }
326
327         my $all_perms = get_all_subpermissions();
328
329         my @flagroots = qw(circulate catalogue parameters borrowers permissions reserveforothers borrow
330           editcatalogue updatecharges tools editauthorities serials reports acquisition clubs problem_reports);
331
332         # We are going to use the $flags returned by checkauth
333         # to create the template's parameters that will indicate
334         # which menus the user can access.
335         if ( $flags && $flags->{superlibrarian} == 1 ) {
336             $template->param( CAN_user_circulate        => 1 );
337             $template->param( CAN_user_catalogue        => 1 );
338             $template->param( CAN_user_parameters       => 1 );
339             $template->param( CAN_user_borrowers        => 1 );
340             $template->param( CAN_user_permissions      => 1 );
341             $template->param( CAN_user_reserveforothers => 1 );
342             $template->param( CAN_user_editcatalogue    => 1 );
343             $template->param( CAN_user_updatecharges    => 1 );
344             $template->param( CAN_user_acquisition      => 1 );
345             $template->param( CAN_user_suggestions      => 1 );
346             $template->param( CAN_user_tools            => 1 );
347             $template->param( CAN_user_editauthorities  => 1 );
348             $template->param( CAN_user_serials          => 1 );
349             $template->param( CAN_user_reports          => 1 );
350             $template->param( CAN_user_staffaccess      => 1 );
351             $template->param( CAN_user_coursereserves   => 1 );
352             $template->param( CAN_user_plugins          => 1 );
353             $template->param( CAN_user_lists            => 1 );
354             $template->param( CAN_user_clubs            => 1 );
355             $template->param( CAN_user_ill              => 1 );
356             $template->param( CAN_user_stockrotation    => 1 );
357             $template->param( CAN_user_cash_management  => 1 );
358             $template->param( CAN_user_problem_reports  => 1 );
359             $template->param( CAN_user_recalls          => 1 );
360
361             foreach my $module ( keys %$all_perms ) {
362                 foreach my $subperm ( keys %{ $all_perms->{$module} } ) {
363                     $template->param( "CAN_user_${module}_${subperm}" => 1 );
364                 }
365             }
366         }
367
368         if ($flags) {
369             foreach my $module ( keys %$all_perms ) {
370                 if ( defined($flags->{$module}) && $flags->{$module} == 1 ) {
371                     foreach my $subperm ( keys %{ $all_perms->{$module} } ) {
372                         $template->param( "CAN_user_${module}_${subperm}" => 1 );
373                     }
374                 } elsif ( ref( $flags->{$module} ) ) {
375                     foreach my $subperm ( keys %{ $flags->{$module} } ) {
376                         $template->param( "CAN_user_${module}_${subperm}" => 1 );
377                     }
378                 }
379             }
380         }
381
382         if ($flags) {
383             foreach my $module ( keys %$flags ) {
384                 if ( $flags->{$module} == 1 or ref( $flags->{$module} ) ) {
385                     $template->param( "CAN_user_$module" => 1 );
386                 }
387             }
388         }
389
390         # Logged-in opac search history
391         # If the requested template is an opac one and opac search history is enabled
392         if ( $in->{type} eq 'opac' && C4::Context->preference('EnableOpacSearchHistory') ) {
393             my $dbh   = C4::Context->dbh;
394             my $query = "SELECT COUNT(*) FROM search_history WHERE userid=?";
395             my $sth   = $dbh->prepare($query);
396             $sth->execute($borrowernumber);
397
398             # If at least one search has already been performed
399             if ( $sth->fetchrow_array > 0 ) {
400
401                 # We show the link in opac
402                 $template->param( EnableOpacSearchHistory => 1 );
403             }
404             if (C4::Context->preference('LoadSearchHistoryToTheFirstLoggedUser'))
405             {
406                 # And if there are searches performed when the user was not logged in,
407                 # we add them to the logged-in search history
408                 my @recentSearches = C4::Search::History::get_from_session( { cgi => $in->{'query'} } );
409                 if (@recentSearches) {
410                     my $query = q{
411                         INSERT INTO search_history(userid, sessionid, query_desc, query_cgi, type,  total, time )
412                         VALUES (?, ?, ?, ?, ?, ?, ?)
413                     };
414                     my $sth = $dbh->prepare($query);
415                     $sth->execute( $borrowernumber,
416                         $in->{query}->cookie("CGISESSID"),
417                         $_->{query_desc},
418                         $_->{query_cgi},
419                         $_->{type} || 'biblio',
420                         $_->{total},
421                         $_->{time},
422                     ) foreach @recentSearches;
423
424                     # clear out the search history from the session now that
425                     # we've saved it to the database
426                  }
427               }
428               C4::Search::History::set_to_session( { cgi => $in->{'query'}, search_history => [] } );
429
430         } elsif ( $in->{type} eq 'intranet' and C4::Context->preference('EnableSearchHistory') ) {
431             $template->param( EnableSearchHistory => 1 );
432         }
433     }
434     else {    # if this is an anonymous session, setup to display public lists...
435
436         # If shibboleth is enabled, and we're in an anonymous session, we should allow
437         # the user to attempt login via shibboleth.
438         if ($shib) {
439             $template->param( shibbolethAuthentication => $shib,
440                 shibbolethLoginUrl => login_shib_url( $in->{'query'} ),
441             );
442
443             # If shibboleth is enabled and we have a shibboleth login attribute,
444             # but we are in an anonymous session, then we clearly have an invalid
445             # shibboleth koha account.
446             if ($shib_login) {
447                 $template->param( invalidShibLogin => '1' );
448             }
449         }
450
451         $template->param( sessionID => $sessionID );
452
453         if ( $in->{'type'} eq 'opac' ){
454             require Koha::Virtualshelves;
455             my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
456                 {
457                     public => 1,
458                 }
459             );
460             $template->param(
461                 some_public_shelves  => $some_public_shelves,
462             );
463
464             # Set default branch if one has been passed by the environment.
465             $template->param( default_branch => $ENV{OPAC_BRANCH_DEFAULT} ) if $ENV{OPAC_BRANCH_DEFAULT};
466         }
467     }
468
469     # Sysprefs disabled via URL param
470     # Note that value must be defined in order to override via ENV
471     foreach my $syspref (
472         qw(
473             OPACUserCSS
474             OPACUserJS
475             IntranetUserCSS
476             IntranetUserJS
477             OpacAdditionalStylesheet
478             opaclayoutstylesheet
479             intranetcolorstylesheet
480             intranetstylesheet
481         )
482       )
483     {
484         $ENV{"OVERRIDE_SYSPREF_$syspref"} = q{}
485           if $in->{'query'}->param("DISABLE_SYSPREF_$syspref");
486     }
487
488     # Anonymous opac search history
489     # If opac search history is enabled and at least one search has already been performed
490     if ( C4::Context->preference('EnableOpacSearchHistory') ) {
491         my @recentSearches = C4::Search::History::get_from_session( { cgi => $in->{'query'} } );
492         if (@recentSearches) {
493             $template->param( EnableOpacSearchHistory => 1 );
494         }
495     }
496
497     if ( C4::Context->preference('dateformat') ) {
498         $template->param( dateformat => C4::Context->preference('dateformat') );
499     }
500
501     $template->param(auth_forwarded_hash => scalar $in->{'query'}->param('auth_forwarded_hash'));
502
503     # these template parameters are set the same regardless of $in->{'type'}
504
505     my $minPasswordLength = C4::Context->preference('minPasswordLength');
506     $minPasswordLength = 3 if not $minPasswordLength or $minPasswordLength < 3;
507     $template->param(
508         EnhancedMessagingPreferences                                       => C4::Context->preference('EnhancedMessagingPreferences'),
509         GoogleJackets                                                      => C4::Context->preference("GoogleJackets"),
510         OpenLibraryCovers                                                  => C4::Context->preference("OpenLibraryCovers"),
511         KohaAdminEmailAddress                                              => "" . C4::Context->preference("KohaAdminEmailAddress"),
512         LoginFirstname  => ( C4::Context->userenv ? C4::Context->userenv->{"firstname"} : "Bel" ),
513         LoginSurname    => C4::Context->userenv ? C4::Context->userenv->{"surname"}      : "Inconnu",
514         emailaddress    => C4::Context->userenv ? C4::Context->userenv->{"emailaddress"} : undef,
515         TagsEnabled     => C4::Context->preference("TagsEnabled"),
516         hide_marc       => C4::Context->preference("hide_marc"),
517         item_level_itypes  => C4::Context->preference('item-level_itypes'),
518         patronimages       => C4::Context->preference("patronimages"),
519         singleBranchMode   => ( Koha::Libraries->search->count == 1 ),
520         noItemTypeImages   => C4::Context->preference("noItemTypeImages"),
521         marcflavour        => C4::Context->preference("marcflavour"),
522         OPACBaseURL        => C4::Context->preference('OPACBaseURL'),
523         minPasswordLength  => $minPasswordLength,
524     );
525     if ( $in->{'type'} eq "intranet" ) {
526         $template->param(
527             AmazonCoverImages                                                          => C4::Context->preference("AmazonCoverImages"),
528             AutoLocation                                                               => C4::Context->preference("AutoLocation"),
529             PatronAutoComplete                                                         => C4::Context->preference("PatronAutoComplete"),
530             FRBRizeEditions                                                            => C4::Context->preference("FRBRizeEditions"),
531             IndependentBranches                                                        => C4::Context->preference("IndependentBranches"),
532             IntranetNav                                                                => C4::Context->preference("IntranetNav"),
533             IntranetmainUserblock                                                      => C4::Context->preference("IntranetmainUserblock"),
534             LibraryName                                                                => C4::Context->preference("LibraryName"),
535             advancedMARCEditor                                                         => C4::Context->preference("advancedMARCEditor"),
536             canreservefromotherbranches                                                => C4::Context->preference('canreservefromotherbranches'),
537             intranetcolorstylesheet                                                    => C4::Context->preference("intranetcolorstylesheet"),
538             IntranetFavicon                                                            => C4::Context->preference("IntranetFavicon"),
539             intranetreadinghistory                                                     => C4::Context->preference("intranetreadinghistory"),
540             intranetstylesheet                                                         => C4::Context->preference("intranetstylesheet"),
541             IntranetUserCSS                                                            => C4::Context->preference("IntranetUserCSS"),
542             IntranetUserJS                                                             => C4::Context->preference("IntranetUserJS"),
543             suggestion                                                                 => C4::Context->preference("suggestion"),
544             virtualshelves                                                             => C4::Context->preference("virtualshelves"),
545             StaffSerialIssueDisplayCount                                               => C4::Context->preference("StaffSerialIssueDisplayCount"),
546             EasyAnalyticalRecords                                                      => C4::Context->preference('EasyAnalyticalRecords'),
547             LocalCoverImages                                                           => C4::Context->preference('LocalCoverImages'),
548             OPACLocalCoverImages                                                       => C4::Context->preference('OPACLocalCoverImages'),
549             AllowMultipleCovers                                                        => C4::Context->preference('AllowMultipleCovers'),
550             EnableBorrowerFiles                                                        => C4::Context->preference('EnableBorrowerFiles'),
551             UseCourseReserves                                                          => C4::Context->preference("UseCourseReserves"),
552             useDischarge                                                               => C4::Context->preference('useDischarge'),
553             pending_checkout_notes                                                     => Koha::Checkouts->search({ noteseen => 0 }),
554             plugins_enabled                                                            => C4::Context->config("enable_plugins"),
555         );
556     }
557     else {
558         warn "template type should be OPAC, here it is=[" . $in->{'type'} . "]" unless ( $in->{'type'} eq 'opac' );
559
560         #TODO : replace LibraryName syspref with 'system name', and remove this html processing
561         my $LibraryNameTitle = C4::Context->preference("LibraryName");
562         $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
563         $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
564
565         # clean up the busc param in the session
566         # if the page is not opac-detail and not the "add to list" page
567         # and not the "edit comments" page
568         if ( C4::Context->preference("OpacBrowseResults")
569             && $in->{'template_name'} =~ /opac-(.+)\.(?:tt|tmpl)$/ ) {
570             my $pagename = $1;
571             unless ( $pagename =~ /^(?:MARC|ISBD)?detail$/
572                 or $pagename =~ /^showmarc$/
573                 or $pagename =~ /^addbybiblionumber$/
574                 or $pagename =~ /^review$/ )
575             {
576                 my $sessionSearch = get_session( $sessionID );
577                 $sessionSearch->clear( ["busc"] ) if $sessionSearch;
578             }
579         }
580
581         # variables passed from CGI: opac_css_override and opac_search_limits.
582         my $opac_search_limit   = $ENV{'OPAC_SEARCH_LIMIT'};
583         my $opac_limit_override = $ENV{'OPAC_LIMIT_OVERRIDE'};
584         my $opac_name           = '';
585         if (
586             ( $opac_limit_override && $opac_search_limit && $opac_search_limit =~ /branch:([\w-]+)/ ) ||
587             ( $in->{'query'}->param('limit') && $in->{'query'}->param('limit') =~ /branch:([\w-]+)/ ) ||
588             ( $in->{'query'}->param('limit') && $in->{'query'}->param('limit') =~ /multibranchlimit:(\w+)/ )
589           ) {
590             $opac_name = $1;    # opac_search_limit is a branch, so we use it.
591         } elsif ( $in->{'query'}->param('multibranchlimit') ) {
592             $opac_name = $in->{'query'}->param('multibranchlimit');
593         } elsif ( C4::Context->preference("SearchMyLibraryFirst") && C4::Context->userenv && C4::Context->userenv->{'branch'} ) {
594             $opac_name = C4::Context->userenv->{'branch'};
595         }
596
597         my @search_groups = Koha::Library::Groups->get_search_groups({ interface => 'opac' })->as_list;
598         $template->param(
599             AnonSuggestions                       => "" . C4::Context->preference("AnonSuggestions"),
600             LibrarySearchGroups                   => \@search_groups,
601             opac_name                             => $opac_name,
602             LibraryName                           => "" . C4::Context->preference("LibraryName"),
603             LibraryNameTitle                      => "" . $LibraryNameTitle,
604             OPACAmazonCoverImages                 => C4::Context->preference("OPACAmazonCoverImages"),
605             OPACFRBRizeEditions                   => C4::Context->preference("OPACFRBRizeEditions"),
606             OpacHighlightedWords                  => C4::Context->preference("OpacHighlightedWords"),
607             OPACShelfBrowser                      => "" . C4::Context->preference("OPACShelfBrowser"),
608             OPACURLOpenInNewWindow                => "" . C4::Context->preference("OPACURLOpenInNewWindow"),
609             OPACUserCSS                           => "" . C4::Context->preference("OPACUserCSS"),
610             OpacAuthorities                       => C4::Context->preference("OpacAuthorities"),
611             opac_css_override                     => $ENV{'OPAC_CSS_OVERRIDE'},
612             opac_search_limit                     => $opac_search_limit,
613             opac_limit_override                   => $opac_limit_override,
614             OpacBrowser                           => C4::Context->preference("OpacBrowser"),
615             OpacCloud                             => C4::Context->preference("OpacCloud"),
616             OpacKohaUrl                           => C4::Context->preference("OpacKohaUrl"),
617             OpacPasswordChange                    => C4::Context->preference("OpacPasswordChange"),
618             OPACPatronDetails                     => C4::Context->preference("OPACPatronDetails"),
619             OPACPrivacy                           => C4::Context->preference("OPACPrivacy"),
620             OPACFinesTab                          => C4::Context->preference("OPACFinesTab"),
621             OpacTopissue                          => C4::Context->preference("OpacTopissue"),
622             'Version'                             => C4::Context->preference('Version'),
623             hidelostitems                         => C4::Context->preference("hidelostitems"),
624             mylibraryfirst                        => ( C4::Context->preference("SearchMyLibraryFirst") && C4::Context->userenv ) ? C4::Context->userenv->{'branch'} : '',
625             opacbookbag                           => "" . C4::Context->preference("opacbookbag"),
626             OpacFavicon                           => C4::Context->preference("OpacFavicon"),
627             opaclanguagesdisplay                  => "" . C4::Context->preference("opaclanguagesdisplay"),
628             opacreadinghistory                    => C4::Context->preference("opacreadinghistory"),
629             OPACUserJS                            => C4::Context->preference("OPACUserJS"),
630             opacuserlogin                         => "" . C4::Context->preference("opacuserlogin"),
631             OpenLibrarySearch                     => C4::Context->preference("OpenLibrarySearch"),
632             ShowReviewer                          => C4::Context->preference("ShowReviewer"),
633             ShowReviewerPhoto                     => C4::Context->preference("ShowReviewerPhoto"),
634             suggestion                            => "" . C4::Context->preference("suggestion"),
635             virtualshelves                        => "" . C4::Context->preference("virtualshelves"),
636             OPACSerialIssueDisplayCount           => C4::Context->preference("OPACSerialIssueDisplayCount"),
637             SyndeticsClientCode                   => C4::Context->preference("SyndeticsClientCode"),
638             SyndeticsEnabled                      => C4::Context->preference("SyndeticsEnabled"),
639             SyndeticsCoverImages                  => C4::Context->preference("SyndeticsCoverImages"),
640             SyndeticsTOC                          => C4::Context->preference("SyndeticsTOC"),
641             SyndeticsSummary                      => C4::Context->preference("SyndeticsSummary"),
642             SyndeticsEditions                     => C4::Context->preference("SyndeticsEditions"),
643             SyndeticsExcerpt                      => C4::Context->preference("SyndeticsExcerpt"),
644             SyndeticsReviews                      => C4::Context->preference("SyndeticsReviews"),
645             SyndeticsAuthorNotes                  => C4::Context->preference("SyndeticsAuthorNotes"),
646             SyndeticsAwards                       => C4::Context->preference("SyndeticsAwards"),
647             SyndeticsSeries                       => C4::Context->preference("SyndeticsSeries"),
648             SyndeticsCoverImageSize               => C4::Context->preference("SyndeticsCoverImageSize"),
649             OPACLocalCoverImages                  => C4::Context->preference("OPACLocalCoverImages"),
650             PatronSelfRegistration                => C4::Context->preference("PatronSelfRegistration"),
651             PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"),
652             useDischarge                 => C4::Context->preference('useDischarge'),
653         );
654
655         $template->param( OpacPublic => '1' ) if ( $user || C4::Context->preference("OpacPublic") );
656     }
657
658     # Check if we were asked using parameters to force a specific language
659     if ( defined $in->{'query'}->param('language') ) {
660
661         # Extract the language, let C4::Languages::getlanguage choose
662         # what to do
663         my $language = C4::Languages::getlanguage( $in->{'query'} );
664         my $languagecookie = C4::Templates::getlanguagecookie( $in->{'query'}, $language );
665         $cookie = $cookie_mgr->replace_in_list( $cookie, $languagecookie );
666     }
667
668     return ( $template, $borrowernumber, $cookie, $flags );
669 }
670
671 =head2 checkauth
672
673   ($userid, $cookie, $sessionID) = &checkauth($query, $noauth, $flagsrequired, $type);
674
675 Verifies that the user is authorized to run this script.  If
676 the user is authorized, a (userid, cookie, session-id, flags)
677 quadruple is returned.  If the user is not authorized but does
678 not have the required privilege (see $flagsrequired below), it
679 displays an error page and exits.  Otherwise, it displays the
680 login page and exits.
681
682 Note that C<&checkauth> will return if and only if the user
683 is authorized, so it should be called early on, before any
684 unfinished operations (e.g., if you've opened a file, then
685 C<&checkauth> won't close it for you).
686
687 C<$query> is the CGI object for the script calling C<&checkauth>.
688
689 The C<$noauth> argument is optional. If it is set, then no
690 authorization is required for the script.
691
692 C<&checkauth> fetches user and session information from C<$query> and
693 ensures that the user is authorized to run scripts that require
694 authorization.
695
696 The C<$flagsrequired> argument specifies the required privileges
697 the user must have if the username and password are correct.
698 It should be specified as a reference-to-hash; keys in the hash
699 should be the "flags" for the user, as specified in the Members
700 intranet module. Any key specified must correspond to a "flag"
701 in the userflags table. E.g., { circulate => 1 } would specify
702 that the user must have the "circulate" privilege in order to
703 proceed. To make sure that access control is correct, the
704 C<$flagsrequired> parameter must be specified correctly.
705
706 Koha also has a concept of sub-permissions, also known as
707 granular permissions.  This makes the value of each key
708 in the C<flagsrequired> hash take on an additional
709 meaning, i.e.,
710
711  1
712
713 The user must have access to all subfunctions of the module
714 specified by the hash key.
715
716  *
717
718 The user must have access to at least one subfunction of the module
719 specified by the hash key.
720
721  specific permission, e.g., 'export_catalog'
722
723 The user must have access to the specific subfunction list, which
724 must correspond to a row in the permissions table.
725
726 The C<$type> argument specifies whether the template should be
727 retrieved from the opac or intranet directory tree.  "opac" is
728 assumed if it is not specified; however, if C<$type> is specified,
729 "intranet" is assumed if it is not "opac".
730
731 If C<$query> does not have a valid session ID associated with it
732 (i.e., the user has not logged in) or if the session has expired,
733 C<&checkauth> presents the user with a login page (from the point of
734 view of the original script, C<&checkauth> does not return). Once the
735 user has authenticated, C<&checkauth> restarts the original script
736 (this time, C<&checkauth> returns).
737
738 The login page is provided using a HTML::Template, which is set in the
739 systempreferences table or at the top of this file. The variable C<$type>
740 selects which template to use, either the opac or the intranet
741 authentification template.
742
743 C<&checkauth> returns a user ID, a cookie, and a session ID. The
744 cookie should be sent back to the browser; it verifies that the user
745 has authenticated.
746
747 =cut
748
749 sub _version_check {
750     my $type  = shift;
751     my $query = shift;
752     my $version;
753
754     # If version syspref is unavailable, it means Koha is being installed,
755     # and so we must redirect to OPAC maintenance page or to the WebInstaller
756     # also, if OpacMaintenance is ON, OPAC should redirect to maintenance
757     if ( C4::Context->preference('OpacMaintenance') && $type eq 'opac' ) {
758         warn "OPAC Install required, redirecting to maintenance";
759         print $query->redirect("/cgi-bin/koha/maintenance.pl");
760         safe_exit;
761     }
762     unless ( $version = C4::Context->preference('Version') ) {    # assignment, not comparison
763         if ( $type ne 'opac' ) {
764             warn "Install required, redirecting to Installer";
765             print $query->redirect("/cgi-bin/koha/installer/install.pl");
766         } else {
767             warn "OPAC Install required, redirecting to maintenance";
768             print $query->redirect("/cgi-bin/koha/maintenance.pl");
769         }
770         safe_exit;
771     }
772
773     # check that database and koha version are the same
774     # there is no DB version, it's a fresh install,
775     # go to web installer
776     # there is a DB version, compare it to the code version
777     my $kohaversion = Koha::version();
778
779     # remove the 3 last . to have a Perl number
780     $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
781     Koha::Logger->get->debug("kohaversion : $kohaversion");
782     if ( $version < $kohaversion ) {
783         my $warning = "Database update needed, redirecting to %s. Database is $version and Koha is $kohaversion";
784         if ( $type ne 'opac' ) {
785             warn sprintf( $warning, 'Installer' );
786             print $query->redirect("/cgi-bin/koha/installer/install.pl?step=1&op=updatestructure");
787         } else {
788             warn sprintf( "OPAC: " . $warning, 'maintenance' );
789             print $query->redirect("/cgi-bin/koha/maintenance.pl");
790         }
791         safe_exit;
792     }
793 }
794
795 sub _timeout_syspref {
796     my $default_timeout = 600;
797     my $timeout = C4::Context->preference('timeout') || $default_timeout;
798
799     # value in days, convert in seconds
800     if ( $timeout =~ /^(\d+)[dD]$/ ) {
801         $timeout = $1 * 86400;
802     }
803     # value in hours, convert in seconds
804     elsif ( $timeout =~ /^(\d+)[hH]$/ ) {
805         $timeout = $1 * 3600;
806     }
807     elsif ( $timeout !~ m/^\d+$/ ) {
808         warn "The value of the system preference 'timeout' is not correct, defaulting to $default_timeout";
809         $timeout = $default_timeout;
810     }
811
812     return $timeout;
813 }
814
815 sub checkauth {
816     my $query = shift;
817
818     # Get shibboleth login attribute
819     my $shib = C4::Context->config('useshibboleth') && shib_ok();
820     my $shib_login = $shib ? get_login_shib() : undef;
821
822     # $authnotrequired will be set for scripts which will run without authentication
823     my $authnotrequired = shift;
824     my $flagsrequired   = shift;
825     my $type            = shift;
826     my $emailaddress    = shift;
827     my $template_name   = shift;
828     my $params          = shift || {};    # do_not_print
829     $type = 'opac' unless $type;
830
831     if ( $type eq 'opac' && !C4::Context->preference("OpacPublic") ) {
832         my @allowed_scripts_for_private_opac = qw(
833           opac-memberentry.tt
834           opac-registration-email-sent.tt
835           opac-registration-confirmation.tt
836           opac-memberentry-update-submitted.tt
837           opac-password-recovery.tt
838           opac-reset-password.tt
839         );
840         $authnotrequired = 0 unless grep { $_ eq $template_name }
841           @allowed_scripts_for_private_opac;
842     }
843
844     my $timeout = _timeout_syspref();
845
846     my $cookie_mgr = Koha::CookieManager->new;
847
848     _version_check( $type, $query );
849
850     # state variables
851     my $auth_state = 'failed';
852     my %info;
853     my ( $userid, $cookie, $sessionID, $flags );
854     $cookie = [];
855     my $logout = $query->param('logout.x');
856
857     my $anon_search_history;
858     my $cas_ticket = '';
859     # This parameter is the name of the CAS server we want to authenticate against,
860     # when using authentication against multiple CAS servers, as configured in Auth_cas_servers.yaml
861     my $casparam = $query->param('cas');
862     my $q_userid = $query->param('userid') // '';
863
864     my $session;
865     my $invalid_otp_token;
866     my $require_2FA = ( C4::Context->preference('TwoFactorAuthentication') && $type ne "opac" ) ? 1 : 0;
867
868     # Basic authentication is incompatible with the use of Shibboleth,
869     # as Shibboleth may return REMOTE_USER as a Shibboleth attribute,
870     # and it may not be the attribute we want to use to match the koha login.
871     #
872     # Also, do not consider an empty REMOTE_USER.
873     #
874     # Finally, after those tests, we can assume (although if it would be better with
875     # a syspref) that if we get a REMOTE_USER, that's from basic authentication,
876     # and we can affect it to $userid.
877     if ( !$shib and defined( $ENV{'REMOTE_USER'} ) and $ENV{'REMOTE_USER'} ne '' and $userid = $ENV{'REMOTE_USER'} ) {
878
879         # Using Basic Authentication, no cookies required
880         $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
881             -name     => 'CGISESSID',
882             -value    => '',
883             -HttpOnly => 1,
884             -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
885             -sameSite => 'Lax',
886         ));
887     }
888     elsif ( $emailaddress) {
889         # the Google OpenID Connect passes an email address
890     }
891     elsif ( $sessionID = $query->cookie("CGISESSID") ) {    # assignment, not comparison
892         my ( $return, $more_info );
893         # NOTE: $flags in the following call is still undefined !
894         ( $return, $session, $more_info ) = check_cookie_auth( $sessionID, $flags,
895             { remote_addr => $ENV{REMOTE_ADDR}, skip_version_check => 1 }
896         );
897
898         if ( $return eq 'ok' || $return eq 'additional-auth-needed' ) {
899             $userid = $session->param('id');
900         }
901
902         $auth_state =
903             $return eq 'ok'                     ? 'completed'
904           : $return eq 'additional-auth-needed' ? 'additional-auth-needed'
905           :                                       'failed';
906
907         # We are at the second screen if the waiting-for-2FA is set in session
908         # and otp_token param has been passed
909         if (   $require_2FA
910             && $auth_state eq 'additional-auth-needed'
911             && ( my $otp_token = $query->param('otp_token') ) )
912         {
913             my $patron    = Koha::Patrons->find( { userid => $userid } );
914             my $auth      = Koha::Auth::TwoFactorAuth->new( { patron => $patron } );
915             my $verified = $auth->verify($otp_token, 1);
916             $auth->clear;
917             if ( $verified ) {
918                 # The token is correct, the user is fully logged in!
919                 $auth_state = 'completed';
920                 $session->param( 'waiting-for-2FA', 0 );
921
922                # This is an ugly trick to pass the test
923                # $query->param('koha_login_context') && ( $q_userid ne $userid )
924                # few lines later
925                 $q_userid = $userid;
926             }
927             else {
928                 $invalid_otp_token = 1;
929             }
930         }
931
932         if ( $auth_state eq 'completed' ) {
933             Koha::Logger->get->debug(sprintf "AUTH_SESSION: (%s)\t%s %s - %s", map { $session->param($_) || q{} } qw(cardnumber firstname surname branch));
934
935             if ( ( $query->param('koha_login_context') && ( $q_userid ne $userid ) )
936                 || ( $cas && $query->param('ticket') && !C4::Context->userenv->{'id'} )
937                 || ( $shib && $shib_login && !$logout && !C4::Context->userenv->{'id'} )
938             ) {
939
940                 #if a user enters an id ne to the id in the current session, we need to log them in...
941                 #first we need to clear the anonymous session...
942                 $anon_search_history = $session->param('search_history');
943                 $session->delete();
944                 $session->flush;
945                 $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie );
946                 C4::Context::_unset_userenv($sessionID);
947                 $sessionID = undef;
948                 undef $userid; # IMPORTANT: this assures us a new session in code below
949                 $auth_state = 'failed';
950             } elsif (!$logout) {
951
952                 $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
953                     -name     => 'CGISESSID',
954                     -value    => $session->id,
955                     -HttpOnly => 1,
956                     -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
957                     -sameSite => 'Lax',
958                 ));
959
960                 $flags = haspermission( $userid, $flagsrequired );
961                 unless ( $flags ) {
962                     $auth_state = 'failed';
963                     $info{'nopermission'} = 1;
964                 }
965             }
966         } elsif ( !$logout ) {
967             if ( $return eq 'expired' ) {
968                 $info{timed_out} = 1;
969             } elsif ( $return eq 'restricted' ) {
970                 $info{oldip}        = $more_info->{old_ip};
971                 $info{newip}        = $more_info->{new_ip};
972                 $info{different_ip} = 1;
973             } elsif ( $return eq 'password_expired' ) {
974                 $info{password_has_expired} = 1;
975             }
976         }
977     }
978
979     if ( $auth_state eq 'failed' || $logout ) {
980         $sessionID = undef;
981         $userid    = undef;
982     }
983
984     if ($logout) {
985
986         # voluntary logout the user
987         # check wether the user was using their shibboleth session or a local one
988         my $shibSuccess = C4::Context->userenv ? C4::Context->userenv->{'shibboleth'} : undef;
989         if ( $session ) {
990             $session->delete();
991             $session->flush;
992         }
993         C4::Context::_unset_userenv($sessionID);
994         $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie );
995
996         if ($cas and $caslogout) {
997             logout_cas($query, $type);
998         }
999
1000         # If we are in a shibboleth session (shibboleth is enabled, a shibboleth match attribute is set and matches koha matchpoint)
1001         if ( $shib and $shib_login and $shibSuccess) {
1002             logout_shib($query);
1003         }
1004
1005         $session   = undef;
1006         $auth_state = 'logout';
1007     }
1008
1009     unless ( $userid ) {
1010         #we initiate a session prior to checking for a username to allow for anonymous sessions...
1011         if( !$session or !$sessionID ) { # if we cleared sessionID, we need a new session
1012             $session = get_session() or die "Auth ERROR: Cannot get_session()";
1013         }
1014
1015         # Save anonymous search history in new session so it can be retrieved
1016         # by get_template_and_user to store it in user's search history after
1017         # a successful login.
1018         if ($anon_search_history) {
1019             $session->param( 'search_history', $anon_search_history );
1020         }
1021
1022         $sessionID = $session->id;
1023         C4::Context->_new_userenv($sessionID);
1024         $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
1025             -name     => 'CGISESSID',
1026             -value    => $sessionID,
1027             -HttpOnly => 1,
1028             -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1029             -sameSite => 'Lax',
1030         ));
1031         my $pki_field = C4::Context->preference('AllowPKIAuth');
1032         if ( !defined($pki_field) ) {
1033             print STDERR "ERROR: Missing system preference AllowPKIAuth.\n";
1034             $pki_field = 'None';
1035         }
1036         if ( ( $cas && $query->param('ticket') )
1037             || $q_userid
1038             || ( $shib && $shib_login )
1039             || $pki_field ne 'None'
1040             || $emailaddress )
1041         {
1042             my $password    = $query->param('password');
1043             my $shibSuccess = 0;
1044             my ( $return, $cardnumber );
1045
1046             # If shib is enabled and we have a shib login, does the login match a valid koha user
1047             if ( $shib && $shib_login ) {
1048                 my $retuserid;
1049
1050                 # Do not pass password here, else shib will not be checked in checkpw.
1051                 ( $return, $cardnumber, $retuserid ) = checkpw( $q_userid, undef, $query );
1052                 $userid      = $retuserid;
1053                 $shibSuccess = $return;
1054                 $info{'invalidShibLogin'} = 1 unless ($return);
1055             }
1056
1057             # If shib login and match were successful, skip further login methods
1058             unless ($shibSuccess) {
1059                 if ( $cas && $query->param('ticket') ) {
1060                     my $retuserid;
1061                     ( $return, $cardnumber, $retuserid, $cas_ticket ) =
1062                       checkpw( $userid, $password, $query, $type );
1063                     $userid = $retuserid;
1064                     $info{'invalidCasLogin'} = 1 unless ($return);
1065                 }
1066
1067                 elsif ( $emailaddress ) {
1068                     my $value = $emailaddress;
1069
1070                     # If we're looking up the email, there's a chance that the person
1071                     # doesn't have a userid. So if there is none, we pass along the
1072                     # borrower number, and the bits of code that need to know the user
1073                     # ID will have to be smart enough to handle that.
1074                     my $patrons = Koha::Patrons->search({ email => $value });
1075                     if ($patrons->count) {
1076
1077                         # First the userid, then the borrowernum
1078                         my $patron = $patrons->next;
1079                         $value = $patron->userid || $patron->borrowernumber;
1080                     } else {
1081                         undef $value;
1082                     }
1083                     $return = $value ? 1 : 0;
1084                     $userid = $value;
1085                 }
1086
1087                 elsif (
1088                     ( $pki_field eq 'Common Name' && $ENV{'SSL_CLIENT_S_DN_CN'} )
1089                     || ( $pki_field eq 'emailAddress'
1090                         && $ENV{'SSL_CLIENT_S_DN_Email'} )
1091                   )
1092                 {
1093                     my $value;
1094                     if ( $pki_field eq 'Common Name' ) {
1095                         $value = $ENV{'SSL_CLIENT_S_DN_CN'};
1096                     }
1097                     elsif ( $pki_field eq 'emailAddress' ) {
1098                         $value = $ENV{'SSL_CLIENT_S_DN_Email'};
1099
1100                         # If we're looking up the email, there's a chance that the person
1101                         # doesn't have a userid. So if there is none, we pass along the
1102                         # borrower number, and the bits of code that need to know the user
1103                         # ID will have to be smart enough to handle that.
1104                         my $patrons = Koha::Patrons->search({ email => $value });
1105                         if ($patrons->count) {
1106
1107                             # First the userid, then the borrowernum
1108                             my $patron = $patrons->next;
1109                             $value = $patron->userid || $patron->borrowernumber;
1110                         } else {
1111                             undef $value;
1112                         }
1113                     }
1114
1115                     $return = $value ? 1 : 0;
1116                     $userid = $value;
1117
1118                 }
1119                 else {
1120                     my $retuserid;
1121                     my $request_method = $query->request_method();
1122
1123                     if (
1124                         $request_method eq 'POST'
1125                         || ( C4::Context->preference('AutoSelfCheckID')
1126                             && $q_userid eq C4::Context->preference('AutoSelfCheckID') )
1127                       )
1128                     {
1129
1130                         ( $return, $cardnumber, $retuserid, $cas_ticket ) =
1131                           checkpw( $q_userid, $password, $query, $type );
1132                         $userid = $retuserid if ($retuserid);
1133                         $info{'invalid_username_or_password'} = 1 unless ($return);
1134                     }
1135                 }
1136             }
1137
1138             # If shib configured and shibOnly enabled, we should ignore anything other than a shibboleth type login.
1139             if (
1140                    $shib
1141                 && !$shibSuccess
1142                 && (
1143                     (
1144                         ( $type eq 'opac' )
1145                         && C4::Context->preference('OPACShibOnly')
1146                     )
1147                     || ( ( $type ne 'opac' )
1148                         && C4::Context->preference('staffShibOnly') )
1149                 )
1150               )
1151             {
1152                 $return = 0;
1153             }
1154
1155             # $return: 1 = valid user
1156             if ($return > 0) {
1157
1158                 if ( $flags = haspermission( $userid, $flagsrequired ) ) {
1159                     $auth_state = "logged_in";
1160                 }
1161                 else {
1162                     $auth_state = 'failed';
1163                     # FIXME We could add $return = 0; or even delete the session?
1164                     # Currently return == 1 and we will fill session info later on,
1165                     # although we do present an authorization failure. (Yes, the
1166                     # authentication was actually correct.)
1167                     $info{'nopermission'} = 1;
1168                     C4::Context::_unset_userenv($sessionID);
1169                 }
1170                 my ( $borrowernumber, $firstname, $surname, $userflags,
1171                     $branchcode, $branchname, $emailaddress, $desk_id,
1172                     $desk_name, $register_id, $register_name );
1173
1174                 if ( $return == 1 ) {
1175                     my $select = "
1176                     SELECT borrowernumber, firstname, surname, flags, borrowers.branchcode,
1177                     branches.branchname    as branchname, email
1178                     FROM borrowers
1179                     LEFT JOIN branches on borrowers.branchcode=branches.branchcode
1180                     ";
1181                     my $dbh = C4::Context->dbh;
1182                     my $sth = $dbh->prepare("$select where userid=?");
1183                     $sth->execute($userid);
1184                     unless ( $sth->rows ) {
1185                         $sth = $dbh->prepare("$select where cardnumber=?");
1186                         $sth->execute($cardnumber);
1187
1188                         unless ( $sth->rows ) {
1189                             $sth->execute($userid);
1190                         }
1191                     }
1192                     if ( $sth->rows ) {
1193                         ( $borrowernumber, $firstname, $surname, $userflags,
1194                             $branchcode, $branchname, $emailaddress ) = $sth->fetchrow;
1195                     }
1196
1197                     # launch a sequence to check if we have a ip for the branch, i
1198                     # if we have one we replace the branchcode of the userenv by the branch bound in the ip.
1199
1200                     my $ip = $ENV{'REMOTE_ADDR'};
1201
1202                     # if they specify at login, use that
1203                     if ( $query->param('branch') ) {
1204                         $branchcode = $query->param('branch');
1205                         my $library = Koha::Libraries->find($branchcode);
1206                         $branchname = $library? $library->branchname: '';
1207                     }
1208                     if ( $query->param('desk_id') ) {
1209                         $desk_id = $query->param('desk_id');
1210                         my $desk = Koha::Desks->find($desk_id);
1211                         $desk_name = $desk ? $desk->desk_name : '';
1212                     }
1213                     if ( C4::Context->preference('UseCashRegisters') ) {
1214                         my $register =
1215                           $query->param('register_id')
1216                           ? Koha::Cash::Registers->find($query->param('register_id'))
1217                           : Koha::Cash::Registers->search(
1218                             { branch => $branchcode, branch_default => 1 },
1219                             { rows   => 1 } )->single;
1220                         $register_id   = $register->id   if ($register);
1221                         $register_name = $register->name if ($register);
1222                     }
1223                     my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search->as_list };
1224                     if ( $type ne 'opac' and C4::Context->preference('AutoLocation') ) {
1225
1226                         # we have to check they are coming from the right ip range
1227                         my $domain = $branches->{$branchcode}->{'branchip'};
1228                         $domain =~ s|\.\*||g;
1229                         if ( $ip !~ /^$domain/ ) {
1230                             $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
1231                                 -name     => 'CGISESSID',
1232                                 -value    => '',
1233                                 -HttpOnly => 1,
1234                                 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1235                                 -sameSite => 'Lax',
1236                             ));
1237                             $info{'wrongip'} = 1;
1238                             $auth_state = "failed";
1239                         }
1240                     }
1241
1242                     if ( C4::Context->preference('AutoLocation') && $auth_state ne 'failed' ) {
1243                         foreach my $br ( uniq( $branchcode, keys %$branches ) ) {
1244
1245                             #     now we work with the treatment of ip
1246                             my $domain = $branches->{$br}->{'branchip'};
1247                             if ( $domain && $ip =~ /^$domain/ ) {
1248                                 $branchcode = $branches->{$br}->{'branchcode'};
1249
1250                                 # new op dev : add the branchname to the cookie
1251                                 $branchname = $branches->{$br}->{'branchname'};
1252                                 last;
1253                             }
1254                         }
1255                     }
1256
1257                     my $is_sco_user = 0;
1258                     if ( $query->param('sco_user_login') && ( $query->param('sco_user_login') eq '1' ) ){
1259                         $is_sco_user = 1;
1260                     }
1261
1262                     $session->param( 'number',       $borrowernumber );
1263                     $session->param( 'id',           $userid );
1264                     $session->param( 'cardnumber',   $cardnumber );
1265                     $session->param( 'firstname',    $firstname );
1266                     $session->param( 'surname',      $surname );
1267                     $session->param( 'branch',       $branchcode );
1268                     $session->param( 'branchname',   $branchname );
1269                     $session->param( 'desk_id',      $desk_id);
1270                     $session->param( 'desk_name',     $desk_name);
1271                     $session->param( 'flags',        $userflags );
1272                     $session->param( 'emailaddress', $emailaddress );
1273                     $session->param( 'ip',           $session->remote_addr() );
1274                     $session->param( 'lasttime',     time() );
1275                     $session->param( 'interface',    $type);
1276                     $session->param( 'shibboleth',   $shibSuccess );
1277                     $session->param( 'register_id',  $register_id );
1278                     $session->param( 'register_name',  $register_name );
1279                     $session->param( 'sco_user', $is_sco_user );
1280                 }
1281                 $session->param('cas_ticket', $cas_ticket) if $cas_ticket;
1282                 C4::Context->set_userenv(
1283                     $session->param('number'),       $session->param('id'),
1284                     $session->param('cardnumber'),   $session->param('firstname'),
1285                     $session->param('surname'),      $session->param('branch'),
1286                     $session->param('branchname'),   $session->param('flags'),
1287                     $session->param('emailaddress'), $session->param('shibboleth'),
1288                     $session->param('desk_id'),      $session->param('desk_name'),
1289                     $session->param('register_id'),  $session->param('register_name')
1290                 );
1291
1292             }
1293             # $return: 0 = invalid user
1294             # reset to anonymous session
1295             else {
1296                 if ($userid) {
1297                     $info{'invalid_username_or_password'} = 1;
1298                     C4::Context::_unset_userenv($sessionID);
1299                 }
1300                 $session->param( 'lasttime', time() );
1301                 $session->param( 'ip',       $session->remote_addr() );
1302                 $session->param( 'sessiontype', 'anon' );
1303                 $session->param( 'interface', $type);
1304             }
1305         }    # END if ( $q_userid
1306         elsif ( $type eq "opac" ) {
1307
1308             # anonymous sessions are created only for the OPAC
1309
1310             # setting a couple of other session vars...
1311             $session->param( 'ip',          $session->remote_addr() );
1312             $session->param( 'lasttime',    time() );
1313             $session->param( 'sessiontype', 'anon' );
1314             $session->param( 'interface', $type);
1315         }
1316         $session->flush;
1317     }    # END unless ($userid)
1318
1319
1320     if ( $auth_state eq 'logged_in' ) {
1321         $auth_state = 'completed';
1322
1323         # Auth is completed unless an additional auth is needed
1324         if ( $require_2FA ) {
1325             my $patron = Koha::Patrons->find({userid => $userid});
1326             if ( $patron->auth_method eq 'two-factor' ) {
1327                 # Ask for the OTP token
1328                 $auth_state = 'additional-auth-needed';
1329                 $session->param('waiting-for-2FA', 1);
1330                 %info = ();# We remove the warnings/errors we may have set incorrectly before
1331             }
1332         }
1333     }
1334
1335     # finished authentification, now respond
1336     if ( $auth_state eq 'completed' || $authnotrequired ) {
1337         # successful login
1338         unless (@$cookie) {
1339             $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
1340                 -name     => 'CGISESSID',
1341                 -value    => '',
1342                 -HttpOnly => 1,
1343                 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1344                 -sameSite => 'Lax',
1345             ));
1346         }
1347
1348         track_login_daily( $userid );
1349
1350         # In case, that this request was a login attempt, we want to prevent that users can repost the opac login
1351         # request. We therefore redirect the user to the requested page again without the login parameters.
1352         # See Post/Redirect/Get (PRG) design pattern: https://en.wikipedia.org/wiki/Post/Redirect/Get
1353         if ( $type eq "opac" && $query->param('koha_login_context') && $query->param('koha_login_context') ne 'sco' && $query->param('password') && $query->param('userid') ) {
1354             my $uri = URI->new($query->url(-relative=>1, -query_string=>1));
1355             $uri->query_param_delete('userid');
1356             $uri->query_param_delete('password');
1357             $uri->query_param_delete('koha_login_context');
1358             unless ( $params->{do_not_print} ) {
1359                 print $query->redirect( -uri => $uri->as_string, -cookie => $cookie, -status => '303 See other' );
1360                 safe_exit;
1361             }
1362         }
1363
1364         return ( $userid, $cookie, $sessionID, $flags );
1365     }
1366
1367     #
1368     #
1369     # AUTH rejected, show the login/password template, after checking the DB.
1370     #
1371     #
1372
1373     my $patron = Koha::Patrons->find({ userid => $q_userid }); # Not necessary logged in!
1374
1375     # get the inputs from the incoming query
1376     my @inputs = ();
1377     my @inputs_to_clean = qw( userid password ticket logout.x otp_token );
1378     foreach my $name ( param $query) {
1379         next if grep { $name eq $_ } @inputs_to_clean;
1380         my @value = $query->multi_param($name);
1381         push @inputs, { name => $name, value => $_ } for @value;
1382     }
1383
1384     my $LibraryNameTitle = C4::Context->preference("LibraryName");
1385     $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
1386     $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
1387
1388     my $auth_template_name = ( $type eq 'opac' ) ? 'opac-auth.tt' : 'auth.tt';
1389     my $template = C4::Templates::gettemplate( $auth_template_name, $type, $query );
1390     $template->param(
1391         login                                 => 1,
1392         INPUTS                                => \@inputs,
1393         script_name                           => get_script_name(),
1394         casAuthentication                     => C4::Context->preference("casAuthentication"),
1395         shibbolethAuthentication              => $shib,
1396         suggestion                            => C4::Context->preference("suggestion"),
1397         virtualshelves                        => C4::Context->preference("virtualshelves"),
1398         LibraryName                           => "" . C4::Context->preference("LibraryName"),
1399         LibraryNameTitle                      => "" . $LibraryNameTitle,
1400         opacuserlogin                         => C4::Context->preference("opacuserlogin"),
1401         OpacFavicon                           => C4::Context->preference("OpacFavicon"),
1402         opacreadinghistory                    => C4::Context->preference("opacreadinghistory"),
1403         opaclanguagesdisplay                  => C4::Context->preference("opaclanguagesdisplay"),
1404         OPACUserJS                            => C4::Context->preference("OPACUserJS"),
1405         OPACUserCSS                           => C4::Context->preference("OPACUserCSS"),
1406         intranetcolorstylesheet               => C4::Context->preference("intranetcolorstylesheet"),
1407         intranetstylesheet                    => C4::Context->preference("intranetstylesheet"),
1408         IntranetNav                           => C4::Context->preference("IntranetNav"),
1409         IntranetFavicon                       => C4::Context->preference("IntranetFavicon"),
1410         IntranetUserCSS                       => C4::Context->preference("IntranetUserCSS"),
1411         IntranetUserJS                        => C4::Context->preference("IntranetUserJS"),
1412         IndependentBranches                   => C4::Context->preference("IndependentBranches"),
1413         AutoLocation                          => C4::Context->preference("AutoLocation"),
1414         wrongip                               => $info{'wrongip'},
1415         PatronSelfRegistration                => C4::Context->preference("PatronSelfRegistration"),
1416         PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"),
1417         opac_css_override                     => $ENV{'OPAC_CSS_OVERRIDE'},
1418         too_many_login_attempts               => ( $patron and $patron->account_locked ),
1419         password_has_expired                  => ( $patron and $patron->password_expired ),
1420     );
1421
1422     $template->param( SCO_login => 1 ) if ( $query->param('sco_user_login') );
1423     $template->param( SCI_login => 1 ) if ( $query->param('sci_user_login') );
1424     $template->param( OpacPublic => C4::Context->preference("OpacPublic") );
1425     $template->param( loginprompt => 1 ) unless $info{'nopermission'};
1426     if ( $auth_state eq 'additional-auth-needed' ) {
1427         $template->param(
1428             TwoFA_prompt => 1,
1429             invalid_otp_token => $invalid_otp_token,
1430         );
1431     }
1432
1433     if ( $type eq 'opac' ) {
1434         require Koha::Virtualshelves;
1435         my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
1436             {
1437                 public => 1,
1438             }
1439         );
1440         $template->param(
1441             some_public_shelves  => $some_public_shelves,
1442         );
1443     }
1444
1445     if ($cas) {
1446
1447         # Is authentication against multiple CAS servers enabled?
1448         require C4::Auth_with_cas;
1449         if ( multipleAuth() && !$casparam ) {
1450             my $casservers = getMultipleAuth();
1451             my @tmplservers;
1452             foreach my $key ( keys %$casservers ) {
1453                 push @tmplservers, { name => $key, value => login_cas_url( $query, $key, $type ) . "?cas=$key" };
1454             }
1455             $template->param(
1456                 casServersLoop => \@tmplservers
1457             );
1458         } else {
1459             $template->param(
1460                 casServerUrl => login_cas_url($query, undef, $type),
1461             );
1462         }
1463
1464         $template->param(
1465             invalidCasLogin => $info{'invalidCasLogin'}
1466         );
1467     }
1468
1469     if ($shib) {
1470         #If shibOnly is enabled just go ahead and redirect directly
1471         if ( (($type eq 'opac') && C4::Context->preference('OPACShibOnly')) || (($type ne 'opac') && C4::Context->preference('staffShibOnly')) ) {
1472             my $redirect_url = login_shib_url( $query );
1473             print $query->redirect( -uri => "$redirect_url", -status => 303 );
1474             safe_exit;
1475         }
1476
1477         $template->param(
1478             shibbolethAuthentication => $shib,
1479             shibbolethLoginUrl       => login_shib_url($query),
1480         );
1481     }
1482
1483     if (C4::Context->preference('GoogleOpenIDConnect')) {
1484         if ($query->param("OpenIDConnectFailed")) {
1485             my $reason = $query->param('OpenIDConnectFailed');
1486             $template->param(invalidGoogleOpenIDConnectLogin => $reason);
1487         }
1488     }
1489
1490     $template->param(
1491         LibraryName => C4::Context->preference("LibraryName"),
1492     );
1493     $template->param(%info);
1494
1495     if ( $params->{do_not_print} ) {
1496         # This must be used for testing purpose only!
1497         return ( undef, undef, undef, undef, $template );
1498     }
1499
1500     print $query->header(
1501         {   type              => 'text/html',
1502             charset           => 'utf-8',
1503             cookie            => $cookie,
1504             'X-Frame-Options' => 'SAMEORIGIN',
1505             -sameSite => 'Lax'
1506         }
1507       ),
1508       $template->output;
1509     safe_exit;
1510 }
1511
1512 =head2 check_api_auth
1513
1514   ($status, $cookie, $sessionId) = check_api_auth($query, $userflags);
1515
1516 Given a CGI query containing the parameters 'userid' and 'password' and/or a session
1517 cookie, determine if the user has the privileges specified by C<$userflags>.
1518
1519 C<check_api_auth> is is meant for authenticating users of web services, and
1520 consequently will always return and will not attempt to redirect the user
1521 agent.
1522
1523 If a valid session cookie is already present, check_api_auth will return a status
1524 of "ok", the cookie, and the Koha session ID.
1525
1526 If no session cookie is present, check_api_auth will check the 'userid' and 'password
1527 parameters and create a session cookie and Koha session if the supplied credentials
1528 are OK.
1529
1530 Possible return values in C<$status> are:
1531
1532 =over
1533
1534 =item "ok" -- user authenticated; C<$cookie> and C<$sessionid> have valid values.
1535
1536 =item "failed" -- credentials are not correct; C<$cookie> and C<$sessionid> are undef
1537
1538 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1539
1540 =item "expired -- session cookie has expired; API user should resubmit userid and password
1541
1542 =item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1543
1544 =item "additional-auth-needed -- User is in an authentication process that is not finished
1545
1546 =back
1547
1548 =cut
1549
1550 sub check_api_auth {
1551
1552     my $query         = shift;
1553     my $flagsrequired = shift;
1554     my $timeout = _timeout_syspref();
1555
1556     unless ( C4::Context->preference('Version') ) {
1557
1558         # database has not been installed yet
1559         return ( "maintenance", undef, undef );
1560     }
1561     my $kohaversion = Koha::version();
1562     $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1563     if ( C4::Context->preference('Version') < $kohaversion ) {
1564
1565         # database in need of version update; assume that
1566         # no API should be called while databsae is in
1567         # this condition.
1568         return ( "maintenance", undef, undef );
1569     }
1570
1571     my ( $sessionID, $session );
1572     unless ( $query->param('userid') ) {
1573         $sessionID = $query->cookie("CGISESSID");
1574     }
1575     if ( $sessionID && not( $cas && $query->param('PT') ) ) {
1576
1577         my $return;
1578         ( $return, $session, undef ) = check_cookie_auth(
1579             $sessionID, $flagsrequired, { remote_addr => $ENV{REMOTE_ADDR} } );
1580
1581         return ( $return, undef, undef ) # Cookie auth failed
1582             if $return ne "ok";
1583
1584         my $cookie = $query->cookie(
1585             -name     => 'CGISESSID',
1586             -value    => $session->id,
1587             -HttpOnly => 1,
1588             -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1589             -sameSite => 'Lax'
1590         );
1591         return ( $return, $cookie, $session ); # return == 'ok' here
1592
1593     } else {
1594
1595         # new login
1596         my $userid   = $query->param('userid');
1597         my $password = $query->param('password');
1598         my ( $return, $cardnumber, $cas_ticket );
1599
1600         # Proxy CAS auth
1601         if ( $cas && $query->param('PT') ) {
1602             my $retuserid;
1603
1604             # In case of a CAS authentication, we use the ticket instead of the password
1605             my $PT = $query->param('PT');
1606             ( $return, $cardnumber, $userid, $cas_ticket ) = check_api_auth_cas( $PT, $query );    # EXTERNAL AUTH
1607         } else {
1608
1609             # User / password auth
1610             unless ( $userid and $password ) {
1611
1612                 # caller did something wrong, fail the authenticateion
1613                 return ( "failed", undef, undef );
1614             }
1615             my $newuserid;
1616             ( $return, $cardnumber, $newuserid, $cas_ticket ) = checkpw( $userid, $password, $query );
1617         }
1618
1619         if ( $return and haspermission( $userid, $flagsrequired ) ) {
1620             my $session = get_session("");
1621             return ( "failed", undef, undef ) unless $session;
1622
1623             my $sessionID = $session->id;
1624             C4::Context->_new_userenv($sessionID);
1625             my $cookie = $query->cookie(
1626                 -name     => 'CGISESSID',
1627                 -value    => $sessionID,
1628                 -HttpOnly => 1,
1629                 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1630                 -sameSite => 'Lax'
1631             );
1632             if ( $return == 1 ) {
1633                 my (
1634                     $borrowernumber, $firstname,  $surname,
1635                     $userflags,      $branchcode, $branchname,
1636                     $emailaddress
1637                 );
1638                 my $dbh = C4::Context->dbh;
1639                 my $sth =
1640                   $dbh->prepare(
1641 "select borrowernumber, firstname, surname, flags, borrowers.branchcode, branches.branchname as branchname, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where userid=?"
1642                   );
1643                 $sth->execute($userid);
1644                 (
1645                     $borrowernumber, $firstname,  $surname,
1646                     $userflags,      $branchcode, $branchname,
1647                     $emailaddress
1648                 ) = $sth->fetchrow if ( $sth->rows );
1649
1650                 unless ( $sth->rows ) {
1651                     my $sth = $dbh->prepare(
1652 "select borrowernumber, firstname, surname, flags, borrowers.branchcode, branches.branchname as branchname, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where cardnumber=?"
1653                     );
1654                     $sth->execute($cardnumber);
1655                     (
1656                         $borrowernumber, $firstname,  $surname,
1657                         $userflags,      $branchcode, $branchname,
1658                         $emailaddress
1659                     ) = $sth->fetchrow if ( $sth->rows );
1660
1661                     unless ( $sth->rows ) {
1662                         $sth->execute($userid);
1663                         (
1664                             $borrowernumber, $firstname,  $surname,       $userflags,
1665                             $branchcode,     $branchname, $emailaddress
1666                         ) = $sth->fetchrow if ( $sth->rows );
1667                     }
1668                 }
1669
1670                 my $ip = $ENV{'REMOTE_ADDR'};
1671
1672                 # if they specify at login, use that
1673                 if ( $query->param('branch') ) {
1674                     $branchcode = $query->param('branch');
1675                     my $library = Koha::Libraries->find($branchcode);
1676                     $branchname = $library? $library->branchname: '';
1677                 }
1678                 my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search->as_list };
1679                 foreach my $br ( keys %$branches ) {
1680
1681                     #     now we work with the treatment of ip
1682                     my $domain = $branches->{$br}->{'branchip'};
1683                     if ( $domain && $ip =~ /^$domain/ ) {
1684                         $branchcode = $branches->{$br}->{'branchcode'};
1685
1686                         # new op dev : add the branchname to the cookie
1687                         $branchname    = $branches->{$br}->{'branchname'};
1688                     }
1689                 }
1690                 $session->param( 'number',       $borrowernumber );
1691                 $session->param( 'id',           $userid );
1692                 $session->param( 'cardnumber',   $cardnumber );
1693                 $session->param( 'firstname',    $firstname );
1694                 $session->param( 'surname',      $surname );
1695                 $session->param( 'branch',       $branchcode );
1696                 $session->param( 'branchname',   $branchname );
1697                 $session->param( 'flags',        $userflags );
1698                 $session->param( 'emailaddress', $emailaddress );
1699                 $session->param( 'ip',           $session->remote_addr() );
1700                 $session->param( 'lasttime',     time() );
1701                 $session->param( 'interface',    'api'  );
1702             }
1703             $session->param( 'cas_ticket', $cas_ticket);
1704             C4::Context->set_userenv(
1705                 $session->param('number'),       $session->param('id'),
1706                 $session->param('cardnumber'),   $session->param('firstname'),
1707                 $session->param('surname'),      $session->param('branch'),
1708                 $session->param('branchname'),   $session->param('flags'),
1709                 $session->param('emailaddress'), $session->param('shibboleth'),
1710                 $session->param('desk_id'),      $session->param('desk_name'),
1711                 $session->param('register_id'),  $session->param('register_name')
1712             );
1713             return ( "ok", $cookie, $sessionID );
1714         } else {
1715             return ( "failed", undef, undef );
1716         }
1717     }
1718 }
1719
1720 =head2 check_cookie_auth
1721
1722   ($status, $sessionId) = check_cookie_auth($cookie, $userflags);
1723
1724 Given a CGISESSID cookie set during a previous login to Koha, determine
1725 if the user has the privileges specified by C<$userflags>. C<$userflags>
1726 is passed unaltered into C<haspermission> and as such accepts all options
1727 avaiable to that routine with the one caveat that C<check_api_auth> will
1728 also allow 'undef' to be passed and in such a case the permissions check
1729 will be skipped altogether.
1730
1731 C<check_cookie_auth> is meant for authenticating special services
1732 such as tools/upload-file.pl that are invoked by other pages that
1733 have been authenticated in the usual way.
1734
1735 Possible return values in C<$status> are:
1736
1737 =over
1738
1739 =item "ok" -- user authenticated; C<$sessionID> have valid values.
1740
1741 =item "anon" -- user not authenticated but valid for anonymous session.
1742
1743 =item "failed" -- credentials are not correct; C<$sessionid> are undef
1744
1745 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1746
1747 =item "expired -- session cookie has expired; API user should resubmit userid and password
1748
1749 =item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1750
1751 =back
1752
1753 =cut
1754
1755 sub check_cookie_auth {
1756     my $sessionID     = shift;
1757     my $flagsrequired = shift;
1758     my $params        = shift;
1759
1760     my $remote_addr = $params->{remote_addr} || $ENV{REMOTE_ADDR};
1761
1762     my $skip_version_check = $params->{skip_version_check}; # Only for checkauth
1763
1764     unless ( $skip_version_check ) {
1765         unless ( C4::Context->preference('Version') ) {
1766
1767             # database has not been installed yet
1768             return ( "maintenance", undef );
1769         }
1770         my $kohaversion = Koha::version();
1771         $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1772         if ( C4::Context->preference('Version') < $kohaversion ) {
1773
1774             # database in need of version update; assume that
1775             # no API should be called while databsae is in
1776             # this condition.
1777             return ( "maintenance", undef );
1778         }
1779     }
1780
1781     # see if we have a valid session cookie already
1782     # however, if a userid parameter is present (i.e., from
1783     # a form submission, assume that any current cookie
1784     # is to be ignored
1785     unless ( $sessionID ) {
1786         return ( "failed", undef );
1787     }
1788     C4::Context::_unset_userenv($sessionID); # remove old userenv first
1789     my $session   = get_session($sessionID);
1790     if ($session) {
1791         my $userid   = $session->param('id');
1792         my $ip       = $session->param('ip');
1793         my $lasttime = $session->param('lasttime');
1794         my $timeout = _timeout_syspref();
1795
1796         if ( !$lasttime || ( $lasttime < time() - $timeout ) ) {
1797             # time out
1798             $session->delete();
1799             $session->flush;
1800             return ("expired", undef);
1801
1802         } elsif ( C4::Context->preference('SessionRestrictionByIP') && $ip ne $remote_addr ) {
1803             # IP address changed
1804             $session->delete();
1805             $session->flush;
1806             return ( "restricted", undef, { old_ip => $ip, new_ip => $remote_addr});
1807
1808         } elsif ( $userid ) {
1809             $session->param( 'lasttime', time() );
1810             my $patron = Koha::Patrons->find({ userid => $userid });
1811             $patron = Koha::Patron->find({ cardnumber => $userid }) unless $patron;
1812             return ("password_expired", undef ) if $patron->password_expired;
1813             my $flags = defined($flagsrequired) ? haspermission( $userid, $flagsrequired ) : 1;
1814             if ($flags) {
1815                 C4::Context->_new_userenv($sessionID);
1816                 C4::Context->interface($session->param('interface'));
1817                 C4::Context->set_userenv(
1818                     $session->param('number'),       $session->param('id') // '',
1819                     $session->param('cardnumber'),   $session->param('firstname'),
1820                     $session->param('surname'),      $session->param('branch'),
1821                     $session->param('branchname'),   $session->param('flags'),
1822                     $session->param('emailaddress'), $session->param('shibboleth'),
1823                     $session->param('desk_id'),      $session->param('desk_name'),
1824                     $session->param('register_id'),  $session->param('register_name')
1825                 );
1826                 return ( "additional-auth-needed", $session )
1827                     if $session->param('waiting-for-2FA');
1828
1829                 return ( "ok", $session );
1830             } else {
1831                 $session->delete();
1832                 $session->flush;
1833                 return ( "failed", undef );
1834             }
1835
1836         } else {
1837             C4::Context->_new_userenv($sessionID);
1838             C4::Context->interface($session->param('interface'));
1839             C4::Context->set_userenv( undef, q{} );
1840             return ( "anon", $session );
1841         }
1842     } else {
1843         return ( "expired", undef );
1844     }
1845 }
1846
1847 =head2 get_session
1848
1849   use CGI::Session;
1850   my $session = get_session($sessionID);
1851
1852 Given a session ID, retrieve the CGI::Session object used to store
1853 the session's state.  The session object can be used to store
1854 data that needs to be accessed by different scripts during a
1855 user's session.
1856
1857 If the C<$sessionID> parameter is an empty string, a new session
1858 will be created.
1859
1860 =cut
1861
1862 sub _get_session_params {
1863     my $storage_method = C4::Context->preference('SessionStorage');
1864     if ( $storage_method eq 'mysql' ) {
1865         my $dbh = C4::Context->dbh;
1866         return { dsn => "serializer:yamlxs;driver:MySQL;id:md5", dsn_args => { Handle => $dbh } };
1867     }
1868     elsif ( $storage_method eq 'Pg' ) {
1869         my $dbh = C4::Context->dbh;
1870         return { dsn => "serializer:yamlxs;driver:PostgreSQL;id:md5", dsn_args => { Handle => $dbh } };
1871     }
1872     elsif ( $storage_method eq 'memcached' && Koha::Caches->get_instance->memcached_cache ) {
1873         my $memcached = Koha::Caches->get_instance()->memcached_cache;
1874         return { dsn => "serializer:yamlxs;driver:memcached;id:md5", dsn_args => { Memcached => $memcached } };
1875     }
1876     else {
1877         # catch all defaults to tmp should work on all systems
1878         my $dir = C4::Context::temporary_directory;
1879         my $instance = C4::Context->config( 'database' ); #actually for packages not exactly the instance name, but generally safer to leave it as it is
1880         return { dsn => "serializer:yamlxs;driver:File;id:md5", dsn_args => { Directory => "$dir/cgisess_$instance" } };
1881     }
1882 }
1883
1884 sub get_session {
1885     my $sessionID      = shift;
1886     my $params = _get_session_params();
1887     my $session;
1888     if( $sessionID ) { # find existing
1889         CGI::Session::ErrorHandler->set_error( q{} ); # clear error, cpan issue #111463
1890         $session = CGI::Session->load( $params->{dsn}, $sessionID, $params->{dsn_args} );
1891     } else {
1892         $session = CGI::Session->new( $params->{dsn}, $sessionID, $params->{dsn_args} );
1893         # no need to flush here
1894     }
1895     return $session;
1896 }
1897
1898
1899 # FIXME no_set_userenv may be replaced with force_branchcode_for_userenv
1900 # (or something similar)
1901 # Currently it's only passed from C4::SIP::ILS::Patron::check_password, but
1902 # not having a userenv defined could cause a crash.
1903 sub checkpw {
1904     my ( $userid, $password, $query, $type, $no_set_userenv ) = @_;
1905     $type = 'opac' unless $type;
1906
1907     # Get shibboleth login attribute
1908     my $shib       = C4::Context->config('useshibboleth') && shib_ok();
1909     my $shib_login = $shib ? get_login_shib() : undef;
1910
1911     my @return;
1912     my $patron;
1913     if ( defined $userid ) {
1914         $patron = Koha::Patrons->find( { userid     => $userid } );
1915         $patron = Koha::Patrons->find( { cardnumber => $userid } ) unless $patron;
1916     }
1917     my $check_internal_as_fallback = 0;
1918     my $passwd_ok                  = 0;
1919
1920     # Note: checkpw_* routines returns:
1921     # 1 if auth is ok
1922     # 0 if auth is nok
1923     # -1 if user bind failed (LDAP only)
1924
1925     if ( $patron and ( $patron->account_locked ) ) {
1926
1927         # Nothing to check, account is locked
1928     } elsif ( $ldap && defined($password) ) {
1929         my ( $retval, $retcard, $retuserid );
1930         ( $retval, $retcard, $retuserid, $patron ) = checkpw_ldap(@_);    # EXTERNAL AUTH
1931         if ( $retval == 1 ) {
1932             @return    = ( $retval, $retcard, $retuserid, $patron );
1933             $passwd_ok = 1;
1934         }
1935         $check_internal_as_fallback = 1 if $retval == 0;
1936
1937     } elsif ( $cas && $query && $query->param('ticket') ) {
1938
1939         # In case of a CAS authentication, we use the ticket instead of the password
1940         my $ticket = $query->param('ticket');
1941         $query->delete('ticket');                                   # remove ticket to come back to original URL
1942         my ( $retval, $retcard, $retuserid, $cas_ticket, $patron ) = checkpw_cas( $ticket, $query, $type );    # EXTERNAL AUTH
1943         if ( $retval ) {
1944             @return = ( $retval, $retcard, $retuserid, $patron, $cas_ticket );
1945         } else {
1946             @return = (0);
1947         }
1948         $passwd_ok = $retval;
1949     }
1950
1951     # If we are in a shibboleth session (shibboleth is enabled, and a shibboleth match attribute is present)
1952     # Check for password to asertain whether we want to be testing against shibboleth or another method this
1953     # time around.
1954     elsif ( $shib && $shib_login && !$password ) {
1955
1956         # In case of a Shibboleth authentication, we expect a shibboleth user attribute
1957         # (defined under shibboleth mapping in koha-conf.xml) to contain the login of the
1958         # shibboleth-authenticated user
1959
1960         # Then, we check if it matches a valid koha user
1961         if ($shib_login) {
1962             my ( $retval, $retcard, $retuserid, $patron ) =
1963                 C4::Auth_with_shibboleth::checkpw_shib($shib_login);    # EXTERNAL AUTH
1964             if ($retval) {
1965                 @return = ( $retval, $retcard, $retuserid, $patron );
1966             }
1967             $passwd_ok = $retval;
1968         }
1969     } else {
1970         $check_internal_as_fallback = 1;
1971     }
1972
1973     # INTERNAL AUTH
1974     if ( $check_internal_as_fallback ) {
1975         @return = checkpw_internal( $userid, $password, $no_set_userenv);
1976         push( @return, $patron );
1977         $passwd_ok = 1 if $return[0] > 0; # 1 or 2
1978     }
1979
1980     if ($patron) {
1981         if ($passwd_ok) {
1982             $patron->update( { login_attempts => 0 } );
1983             if ( $patron->password_expired ) {
1984                 @return = ( -2, $patron );
1985             }
1986         } elsif ( !$patron->account_locked ) {
1987             $patron->update( { login_attempts => $patron->login_attempts + 1 } );
1988         }
1989     }
1990
1991     # Optionally log success or failure
1992     if ( $patron && $passwd_ok && C4::Context->preference('AuthSuccessLog') ) {
1993         logaction( 'AUTH', 'SUCCESS', $patron->id, "Valid password for $userid", $type );
1994     } elsif ( !$passwd_ok && C4::Context->preference('AuthFailureLog') ) {
1995         logaction( 'AUTH', 'FAILURE', $patron ? $patron->id : 0, "Wrong password for $userid", $type );
1996     }
1997
1998     return @return;
1999 }
2000
2001 sub checkpw_internal {
2002     my ( $userid, $password, $no_set_userenv ) = @_;
2003
2004     $password = Encode::encode( 'UTF-8', $password )
2005       if Encode::is_utf8($password);
2006
2007     my $dbh = C4::Context->dbh;
2008     my $sth =
2009       $dbh->prepare(
2010         "select password,cardnumber,borrowernumber,userid,firstname,surname,borrowers.branchcode,branches.branchname,flags from borrowers join branches on borrowers.branchcode=branches.branchcode where userid=?"
2011       );
2012     $sth->execute($userid);
2013     if ( $sth->rows ) {
2014         my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
2015             $surname, $branchcode, $branchname, $flags )
2016           = $sth->fetchrow;
2017
2018         if ( checkpw_hash( $password, $stored_hash ) ) {
2019
2020             C4::Context->set_userenv( "$borrowernumber", $userid, $cardnumber,
2021                 $firstname, $surname, $branchcode, $branchname, $flags ) unless $no_set_userenv;
2022             return 1, $cardnumber, $userid;
2023         }
2024     }
2025     $sth =
2026       $dbh->prepare(
2027         "select password,cardnumber,borrowernumber,userid,firstname,surname,borrowers.branchcode,branches.branchname,flags from borrowers join branches on borrowers.branchcode=branches.branchcode where cardnumber=?"
2028       );
2029     $sth->execute($userid);
2030     if ( $sth->rows ) {
2031         my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
2032             $surname, $branchcode, $branchname, $flags )
2033           = $sth->fetchrow;
2034
2035         if ( checkpw_hash( $password, $stored_hash ) ) {
2036
2037             C4::Context->set_userenv( $borrowernumber, $userid, $cardnumber,
2038                 $firstname, $surname, $branchcode, $branchname, $flags ) unless $no_set_userenv;
2039             return 1, $cardnumber, $userid;
2040         }
2041     }
2042     return 0;
2043 }
2044
2045 sub checkpw_hash {
2046     my ( $password, $stored_hash ) = @_;
2047
2048     return if $stored_hash eq '!';
2049
2050     # check what encryption algorithm was implemented: Bcrypt - if the hash starts with '$2' it is Bcrypt else md5
2051     my $hash;
2052     if ( substr( $stored_hash, 0, 2 ) eq '$2' ) {
2053         $hash = hash_password( $password, $stored_hash );
2054     } else {
2055         $hash = md5_base64($password);
2056     }
2057     return $hash eq $stored_hash;
2058 }
2059
2060 =head2 getuserflags
2061
2062     my $authflags = getuserflags($flags, $userid, [$dbh]);
2063
2064 Translates integer flags into permissions strings hash.
2065
2066 C<$flags> is the integer userflags value ( borrowers.userflags )
2067 C<$userid> is the members.userid, used for building subpermissions
2068 C<$authflags> is a hashref of permissions
2069
2070 =cut
2071
2072 sub getuserflags {
2073     my $flags  = shift;
2074     my $userid = shift;
2075     my $dbh    = @_ ? shift : C4::Context->dbh;
2076     my $userflags;
2077     {
2078         # I don't want to do this, but if someone logs in as the database
2079         # user, it would be preferable not to spam them to death with
2080         # numeric warnings. So, we make $flags numeric.
2081         no warnings 'numeric';
2082         $flags += 0;
2083     }
2084     my $sth = $dbh->prepare("SELECT bit, flag, defaulton FROM userflags");
2085     $sth->execute;
2086
2087     while ( my ( $bit, $flag, $defaulton ) = $sth->fetchrow ) {
2088         if ( ( $flags & ( 2**$bit ) ) || $defaulton ) {
2089             $userflags->{$flag} = 1;
2090         }
2091         else {
2092             $userflags->{$flag} = 0;
2093         }
2094     }
2095
2096     # get subpermissions and merge with top-level permissions
2097     my $user_subperms = get_user_subpermissions($userid);
2098     foreach my $module ( keys %$user_subperms ) {
2099         next if $userflags->{$module} == 1;    # user already has permission for everything in this module
2100         $userflags->{$module} = $user_subperms->{$module};
2101     }
2102
2103     return $userflags;
2104 }
2105
2106 =head2 get_user_subpermissions
2107
2108   $user_perm_hashref = get_user_subpermissions($userid);
2109
2110 Given the userid (note, not the borrowernumber) of a staff user,
2111 return a hashref of hashrefs of the specific subpermissions
2112 accorded to the user.  An example return is
2113
2114  {
2115     tools => {
2116         export_catalog => 1,
2117         import_patrons => 1,
2118     }
2119  }
2120
2121 The top-level hash-key is a module or function code from
2122 userflags.flag, while the second-level key is a code
2123 from permissions.
2124
2125 The results of this function do not give a complete picture
2126 of the functions that a staff user can access; it is also
2127 necessary to check borrowers.flags.
2128
2129 =cut
2130
2131 sub get_user_subpermissions {
2132     my $userid = shift;
2133
2134     my $dbh = C4::Context->dbh;
2135     my $sth = $dbh->prepare( "SELECT flag, user_permissions.code
2136                              FROM user_permissions
2137                              JOIN permissions USING (module_bit, code)
2138                              JOIN userflags ON (module_bit = bit)
2139                              JOIN borrowers USING (borrowernumber)
2140                              WHERE userid = ?" );
2141     $sth->execute($userid);
2142
2143     my $user_perms = {};
2144     while ( my $perm = $sth->fetchrow_hashref ) {
2145         $user_perms->{ $perm->{'flag'} }->{ $perm->{'code'} } = 1;
2146     }
2147     return $user_perms;
2148 }
2149
2150 =head2 get_all_subpermissions
2151
2152   my $perm_hashref = get_all_subpermissions();
2153
2154 Returns a hashref of hashrefs defining all specific
2155 permissions currently defined.  The return value
2156 has the same structure as that of C<get_user_subpermissions>,
2157 except that the innermost hash value is the description
2158 of the subpermission.
2159
2160 =cut
2161
2162 sub get_all_subpermissions {
2163     my $dbh = C4::Context->dbh;
2164     my $sth = $dbh->prepare( "SELECT flag, code
2165                              FROM permissions
2166                              JOIN userflags ON (module_bit = bit)" );
2167     $sth->execute();
2168
2169     my $all_perms = {};
2170     while ( my $perm = $sth->fetchrow_hashref ) {
2171         $all_perms->{ $perm->{'flag'} }->{ $perm->{'code'} } = 1;
2172     }
2173     return $all_perms;
2174 }
2175
2176 =head2 haspermission
2177
2178   $flagsrequired = '*';                                 # Any permission at all
2179   $flagsrequired = 'a_flag';                            # a_flag must be satisfied (all subpermissions)
2180   $flagsrequired = [ 'a_flag', 'b_flag' ];              # a_flag OR b_flag must be satisfied
2181   $flagsrequired = { 'a_flag => 1, 'b_flag' => 1 };     # a_flag AND b_flag must be satisfied
2182   $flagsrequired = { 'a_flag' => 'sub_a' };             # sub_a of a_flag must be satisfied
2183   $flagsrequired = { 'a_flag' => [ 'sub_a, 'sub_b' ] }; # sub_a OR sub_b of a_flag must be satisfied
2184
2185   $flags = ($userid, $flagsrequired);
2186
2187 C<$userid> the userid of the member
2188 C<$flags> is a query structure similar to that used by SQL::Abstract that
2189 denotes the combination of flags required. It is a required parameter.
2190
2191 The main logic of this method is that things in arrays are OR'ed, and things
2192 in hashes are AND'ed. The `*` character can be used, at any depth, to denote `ANY`
2193
2194 Returns member's flags or 0 if a permission is not met.
2195
2196 =cut
2197
2198 sub _dispatch {
2199     my ($required, $flags) = @_;
2200
2201     my $ref = ref($required);
2202     if ($ref eq '') {
2203         if ($required eq '*') {
2204             return 0 unless ( $flags or ref( $flags ) );
2205         } else {
2206             return 0 unless ( $flags and (!ref( $flags ) || $flags->{$required} ));
2207         }
2208     } elsif ($ref eq 'HASH') {
2209         foreach my $key (keys %{$required}) {
2210             next if $flags == 1;
2211             my $require = $required->{$key};
2212             my $rflags  = $flags->{$key};
2213             return 0 unless _dispatch($require, $rflags);
2214         }
2215     } elsif ($ref eq 'ARRAY') {
2216         my $satisfied = 0;
2217         foreach my $require ( @{$required} ) {
2218             my $rflags =
2219               ( ref($flags) && !ref($require) && ( $require ne '*' ) )
2220               ? $flags->{$require}
2221               : $flags;
2222             $satisfied++ if _dispatch( $require, $rflags );
2223         }
2224         return 0 unless $satisfied;
2225     } else {
2226         croak "Unexpected structure found: $ref";
2227     }
2228
2229     return $flags;
2230 };
2231
2232 sub haspermission {
2233     my ( $userid, $flagsrequired ) = @_;
2234
2235     #Koha::Exceptions::WrongParameter->throw('$flagsrequired should not be undef')
2236     #  unless defined($flagsrequired);
2237
2238     my $sth = C4::Context->dbh->prepare("SELECT flags FROM borrowers WHERE userid=?");
2239     $sth->execute($userid);
2240     my $row = $sth->fetchrow();
2241     my $flags = getuserflags( $row, $userid );
2242
2243     return $flags unless defined($flagsrequired);
2244     return $flags if $flags->{superlibrarian};
2245     return _dispatch($flagsrequired, $flags);
2246
2247     #FIXME - This fcn should return the failed permission so a suitable error msg can be delivered.
2248 }
2249
2250 =head2 in_iprange
2251
2252   $flags = ($iprange);
2253
2254 C<$iprange> A space separated string describing an IP range. Can include single IPs or ranges
2255
2256 Returns 1 if the remote address is in the provided iprange, or 0 otherwise.
2257
2258 =cut
2259
2260 sub in_iprange {
2261     my ($iprange) = @_;
2262     my $result = 1;
2263     my @allowedipranges = $iprange ? split(' ', $iprange) : ();
2264     if (scalar @allowedipranges > 0) {
2265         my @rangelist;
2266         eval { @rangelist = Net::CIDR::range2cidr(@allowedipranges); }; return 0 if $@;
2267         eval { $result = Net::CIDR::cidrlookup($ENV{'REMOTE_ADDR'}, @rangelist) } || Koha::Logger->get->warn('cidrlookup failed for ' . join(' ',@rangelist) );
2268      }
2269      return $result ? 1 : 0;
2270 }
2271
2272 sub getborrowernumber {
2273     my ($userid) = @_;
2274     my $userenv = C4::Context->userenv;
2275     if ( defined($userenv) && ref($userenv) eq 'HASH' && $userenv->{number} ) {
2276         return $userenv->{number};
2277     }
2278     my $dbh = C4::Context->dbh;
2279     for my $field ( 'userid', 'cardnumber' ) {
2280         my $sth =
2281           $dbh->prepare("select borrowernumber from borrowers where $field=?");
2282         $sth->execute($userid);
2283         if ( $sth->rows ) {
2284             my ($bnumber) = $sth->fetchrow;
2285             return $bnumber;
2286         }
2287     }
2288     return 0;
2289 }
2290
2291 =head2 track_login_daily
2292
2293     track_login_daily( $userid );
2294
2295 Wraps the call to $patron->track_login, the method used to update borrowers.lastseen. We only call track_login once a day.
2296
2297 =cut
2298
2299 sub track_login_daily {
2300     my $userid = shift;
2301     return if !$userid || !C4::Context->preference('TrackLastPatronActivity');
2302
2303     my $cache     = Koha::Caches->get_instance();
2304     my $cache_key = "track_login_" . $userid;
2305     my $cached    = $cache->get_from_cache($cache_key);
2306     my $today = dt_from_string()->ymd;
2307     return if $cached && $cached eq $today;
2308
2309     my $patron = Koha::Patrons->find({ userid => $userid });
2310     return unless $patron;
2311     $patron->track_login;
2312     $cache->set_in_cache( $cache_key, $today );
2313 }
2314
2315 END { }    # module clean-up code here (global destructor)
2316 1;
2317 __END__
2318
2319 =head1 SEE ALSO
2320
2321 CGI(3)
2322
2323 C4::Output(3)
2324
2325 Crypt::Eksblowfish::Bcrypt(3)
2326
2327 Digest::MD5(3)
2328
2329 =cut