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