Bug 27342: Remove dbh from C4::Auth
[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     );
1411
1412     $template->param( SCO_login => 1 ) if ( $query->param('sco_user_login') );
1413     $template->param( SCI_login => 1 ) if ( $query->param('sci_user_login') );
1414     $template->param( OpacPublic => C4::Context->preference("OpacPublic") );
1415     $template->param( loginprompt => 1 ) unless $info{'nopermission'};
1416     if ( $auth_state eq 'additional-auth-needed' ) {
1417         my $patron = Koha::Patrons->find( { userid => $userid } );
1418         $template->param(
1419             TwoFA_prompt => 1,
1420             invalid_otp_token => $invalid_otp_token,
1421             notice_email_address => $patron->notice_email_address, # We could also pass logged_in_user if necessary
1422         );
1423     }
1424
1425     if ( $type eq 'opac' ) {
1426         require Koha::Virtualshelves;
1427         my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
1428             {
1429                 public => 1,
1430             }
1431         );
1432         $template->param(
1433             some_public_shelves  => $some_public_shelves,
1434         );
1435     }
1436
1437     if ($cas) {
1438
1439         # Is authentication against multiple CAS servers enabled?
1440         require C4::Auth_with_cas;
1441         if ( multipleAuth() && !$casparam ) {
1442             my $casservers = getMultipleAuth();
1443             my @tmplservers;
1444             foreach my $key ( keys %$casservers ) {
1445                 push @tmplservers, { name => $key, value => login_cas_url( $query, $key, $type ) . "?cas=$key" };
1446             }
1447             $template->param(
1448                 casServersLoop => \@tmplservers
1449             );
1450         } else {
1451             $template->param(
1452                 casServerUrl => login_cas_url($query, undef, $type),
1453             );
1454         }
1455
1456         $template->param(
1457             invalidCasLogin => $info{'invalidCasLogin'}
1458         );
1459     }
1460
1461     if ($shib) {
1462         #If shibOnly is enabled just go ahead and redirect directly
1463         if ( (($type eq 'opac') && C4::Context->preference('OPACShibOnly')) || (($type ne 'opac') && C4::Context->preference('staffShibOnly')) ) {
1464             my $redirect_url = login_shib_url( $query );
1465             print $query->redirect( -uri => "$redirect_url", -status => 303 );
1466             safe_exit;
1467         }
1468
1469         $template->param(
1470             shibbolethAuthentication => $shib,
1471             shibbolethLoginUrl       => login_shib_url($query),
1472         );
1473     }
1474
1475     if (C4::Context->preference('GoogleOpenIDConnect')) {
1476         if ($query->param("OpenIDConnectFailed")) {
1477             my $reason = $query->param('OpenIDConnectFailed');
1478             $template->param(invalidGoogleOpenIDConnectLogin => $reason);
1479         }
1480     }
1481
1482     $template->param(
1483         LibraryName => C4::Context->preference("LibraryName"),
1484     );
1485     $template->param(%info);
1486
1487     #    $cookie = $query->cookie(CGISESSID => $session->id
1488     #   );
1489     print $query->header(
1490         {   type              => 'text/html',
1491             charset           => 'utf-8',
1492             cookie            => $cookie,
1493             'X-Frame-Options' => 'SAMEORIGIN',
1494             -sameSite => 'Lax'
1495         }
1496       ),
1497       $template->output;
1498     safe_exit;
1499 }
1500
1501 =head2 check_api_auth
1502
1503   ($status, $cookie, $sessionId) = check_api_auth($query, $userflags);
1504
1505 Given a CGI query containing the parameters 'userid' and 'password' and/or a session
1506 cookie, determine if the user has the privileges specified by C<$userflags>.
1507
1508 C<check_api_auth> is is meant for authenticating users of web services, and
1509 consequently will always return and will not attempt to redirect the user
1510 agent.
1511
1512 If a valid session cookie is already present, check_api_auth will return a status
1513 of "ok", the cookie, and the Koha session ID.
1514
1515 If no session cookie is present, check_api_auth will check the 'userid' and 'password
1516 parameters and create a session cookie and Koha session if the supplied credentials
1517 are OK.
1518
1519 Possible return values in C<$status> are:
1520
1521 =over
1522
1523 =item "ok" -- user authenticated; C<$cookie> and C<$sessionid> have valid values.
1524
1525 =item "failed" -- credentials are not correct; C<$cookie> and C<$sessionid> are undef
1526
1527 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1528
1529 =item "expired -- session cookie has expired; API user should resubmit userid and password
1530
1531 =item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1532
1533 =item "additional-auth-needed -- User is in an authentication process that is not finished
1534
1535 =back
1536
1537 =cut
1538
1539 sub check_api_auth {
1540
1541     my $query         = shift;
1542     my $flagsrequired = shift;
1543     my $timeout = _timeout_syspref();
1544
1545     unless ( C4::Context->preference('Version') ) {
1546
1547         # database has not been installed yet
1548         return ( "maintenance", undef, undef );
1549     }
1550     my $kohaversion = Koha::version();
1551     $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1552     if ( C4::Context->preference('Version') < $kohaversion ) {
1553
1554         # database in need of version update; assume that
1555         # no API should be called while databsae is in
1556         # this condition.
1557         return ( "maintenance", undef, undef );
1558     }
1559
1560     my ( $sessionID, $session );
1561     unless ( $query->param('userid') ) {
1562         $sessionID = $query->cookie("CGISESSID");
1563     }
1564     if ( $sessionID && not( $cas && $query->param('PT') ) ) {
1565
1566         my $return;
1567         ( $return, $session, undef ) = check_cookie_auth(
1568             $sessionID, $flagsrequired, { remote_addr => $ENV{REMOTE_ADDR} } );
1569
1570         return ( $return, undef, undef ) # Cookie auth failed
1571             if $return ne "ok";
1572
1573         my $cookie = $query->cookie(
1574             -name     => 'CGISESSID',
1575             -value    => $session->id,
1576             -HttpOnly => 1,
1577             -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1578             -sameSite => 'Lax'
1579         );
1580         return ( $return, $cookie, $session ); # return == 'ok' here
1581
1582     } else {
1583
1584         # new login
1585         my $userid   = $query->param('userid');
1586         my $password = $query->param('password');
1587         my ( $return, $cardnumber, $cas_ticket );
1588
1589         # Proxy CAS auth
1590         if ( $cas && $query->param('PT') ) {
1591             my $retuserid;
1592
1593             # In case of a CAS authentication, we use the ticket instead of the password
1594             my $PT = $query->param('PT');
1595             ( $return, $cardnumber, $userid, $cas_ticket ) = check_api_auth_cas( $PT, $query );    # EXTERNAL AUTH
1596         } else {
1597
1598             # User / password auth
1599             unless ( $userid and $password ) {
1600
1601                 # caller did something wrong, fail the authenticateion
1602                 return ( "failed", undef, undef );
1603             }
1604             my $newuserid;
1605             ( $return, $cardnumber, $newuserid, $cas_ticket ) = checkpw( $userid, $password, $query );
1606         }
1607
1608         if ( $return and haspermission( $userid, $flagsrequired ) ) {
1609             my $session = get_session("");
1610             return ( "failed", undef, undef ) unless $session;
1611
1612             my $sessionID = $session->id;
1613             C4::Context->_new_userenv($sessionID);
1614             my $cookie = $query->cookie(
1615                 -name     => 'CGISESSID',
1616                 -value    => $sessionID,
1617                 -HttpOnly => 1,
1618                 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1619                 -sameSite => 'Lax'
1620             );
1621             if ( $return == 1 ) {
1622                 my (
1623                     $borrowernumber, $firstname,  $surname,
1624                     $userflags,      $branchcode, $branchname,
1625                     $emailaddress
1626                 );
1627                 my $dbh = C4::Context->dbh;
1628                 my $sth =
1629                   $dbh->prepare(
1630 "select borrowernumber, firstname, surname, flags, borrowers.branchcode, branches.branchname as branchname, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where userid=?"
1631                   );
1632                 $sth->execute($userid);
1633                 (
1634                     $borrowernumber, $firstname,  $surname,
1635                     $userflags,      $branchcode, $branchname,
1636                     $emailaddress
1637                 ) = $sth->fetchrow if ( $sth->rows );
1638
1639                 unless ( $sth->rows ) {
1640                     my $sth = $dbh->prepare(
1641 "select borrowernumber, firstname, surname, flags, borrowers.branchcode, branches.branchname as branchname, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where cardnumber=?"
1642                     );
1643                     $sth->execute($cardnumber);
1644                     (
1645                         $borrowernumber, $firstname,  $surname,
1646                         $userflags,      $branchcode, $branchname,
1647                         $emailaddress
1648                     ) = $sth->fetchrow if ( $sth->rows );
1649
1650                     unless ( $sth->rows ) {
1651                         $sth->execute($userid);
1652                         (
1653                             $borrowernumber, $firstname,  $surname,       $userflags,
1654                             $branchcode,     $branchname, $emailaddress
1655                         ) = $sth->fetchrow if ( $sth->rows );
1656                     }
1657                 }
1658
1659                 my $ip = $ENV{'REMOTE_ADDR'};
1660
1661                 # if they specify at login, use that
1662                 if ( $query->param('branch') ) {
1663                     $branchcode = $query->param('branch');
1664                     my $library = Koha::Libraries->find($branchcode);
1665                     $branchname = $library? $library->branchname: '';
1666                 }
1667                 my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search->as_list };
1668                 foreach my $br ( keys %$branches ) {
1669
1670                     #     now we work with the treatment of ip
1671                     my $domain = $branches->{$br}->{'branchip'};
1672                     if ( $domain && $ip =~ /^$domain/ ) {
1673                         $branchcode = $branches->{$br}->{'branchcode'};
1674
1675                         # new op dev : add the branchname to the cookie
1676                         $branchname    = $branches->{$br}->{'branchname'};
1677                     }
1678                 }
1679                 $session->param( 'number',       $borrowernumber );
1680                 $session->param( 'id',           $userid );
1681                 $session->param( 'cardnumber',   $cardnumber );
1682                 $session->param( 'firstname',    $firstname );
1683                 $session->param( 'surname',      $surname );
1684                 $session->param( 'branch',       $branchcode );
1685                 $session->param( 'branchname',   $branchname );
1686                 $session->param( 'flags',        $userflags );
1687                 $session->param( 'emailaddress', $emailaddress );
1688                 $session->param( 'ip',           $session->remote_addr() );
1689                 $session->param( 'lasttime',     time() );
1690                 $session->param( 'interface',    'api'  );
1691             }
1692             $session->param( 'cas_ticket', $cas_ticket);
1693             C4::Context->set_userenv(
1694                 $session->param('number'),       $session->param('id'),
1695                 $session->param('cardnumber'),   $session->param('firstname'),
1696                 $session->param('surname'),      $session->param('branch'),
1697                 $session->param('branchname'),   $session->param('flags'),
1698                 $session->param('emailaddress'), $session->param('shibboleth'),
1699                 $session->param('desk_id'),      $session->param('desk_name'),
1700                 $session->param('register_id'),  $session->param('register_name')
1701             );
1702             return ( "ok", $cookie, $sessionID );
1703         } else {
1704             return ( "failed", undef, undef );
1705         }
1706     }
1707 }
1708
1709 =head2 check_cookie_auth
1710
1711   ($status, $sessionId) = check_cookie_auth($cookie, $userflags);
1712
1713 Given a CGISESSID cookie set during a previous login to Koha, determine
1714 if the user has the privileges specified by C<$userflags>. C<$userflags>
1715 is passed unaltered into C<haspermission> and as such accepts all options
1716 avaiable to that routine with the one caveat that C<check_api_auth> will
1717 also allow 'undef' to be passed and in such a case the permissions check
1718 will be skipped altogether.
1719
1720 C<check_cookie_auth> is meant for authenticating special services
1721 such as tools/upload-file.pl that are invoked by other pages that
1722 have been authenticated in the usual way.
1723
1724 Possible return values in C<$status> are:
1725
1726 =over
1727
1728 =item "ok" -- user authenticated; C<$sessionID> have valid values.
1729
1730 =item "anon" -- user not authenticated but valid for anonymous session.
1731
1732 =item "failed" -- credentials are not correct; C<$sessionid> are undef
1733
1734 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1735
1736 =item "expired -- session cookie has expired; API user should resubmit userid and password
1737
1738 =item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1739
1740 =back
1741
1742 =cut
1743
1744 sub check_cookie_auth {
1745     my $sessionID     = shift;
1746     my $flagsrequired = shift;
1747     my $params        = shift;
1748
1749     my $remote_addr = $params->{remote_addr} || $ENV{REMOTE_ADDR};
1750
1751     my $skip_version_check = $params->{skip_version_check}; # Only for checkauth
1752
1753     unless ( $skip_version_check ) {
1754         unless ( C4::Context->preference('Version') ) {
1755
1756             # database has not been installed yet
1757             return ( "maintenance", undef );
1758         }
1759         my $kohaversion = Koha::version();
1760         $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1761         if ( C4::Context->preference('Version') < $kohaversion ) {
1762
1763             # database in need of version update; assume that
1764             # no API should be called while databsae is in
1765             # this condition.
1766             return ( "maintenance", undef );
1767         }
1768     }
1769
1770     # see if we have a valid session cookie already
1771     # however, if a userid parameter is present (i.e., from
1772     # a form submission, assume that any current cookie
1773     # is to be ignored
1774     unless ( $sessionID ) {
1775         return ( "failed", undef );
1776     }
1777     C4::Context::_unset_userenv($sessionID); # remove old userenv first
1778     my $session   = get_session($sessionID);
1779     if ($session) {
1780         my $userid   = $session->param('id');
1781         my $ip       = $session->param('ip');
1782         my $lasttime = $session->param('lasttime');
1783         my $timeout = _timeout_syspref();
1784
1785         if ( !$lasttime || ( $lasttime < time() - $timeout ) ) {
1786             # time out
1787             $session->delete();
1788             $session->flush;
1789             return ("expired", undef);
1790
1791         } elsif ( C4::Context->preference('SessionRestrictionByIP') && $ip ne $remote_addr ) {
1792             # IP address changed
1793             $session->delete();
1794             $session->flush;
1795             return ( "restricted", undef, { old_ip => $ip, new_ip => $remote_addr});
1796
1797         } elsif ( $userid ) {
1798             $session->param( 'lasttime', time() );
1799             my $patron = Koha::Patrons->find({ userid => $userid });
1800             $patron = Koha::Patron->find({ cardnumber => $userid }) unless $patron;
1801             return ("password_expired", undef ) if $patron->password_expired;
1802             my $flags = defined($flagsrequired) ? haspermission( $userid, $flagsrequired ) : 1;
1803             if ($flags) {
1804                 C4::Context->_new_userenv($sessionID);
1805                 C4::Context->interface($session->param('interface'));
1806                 C4::Context->set_userenv(
1807                     $session->param('number'),       $session->param('id') // '',
1808                     $session->param('cardnumber'),   $session->param('firstname'),
1809                     $session->param('surname'),      $session->param('branch'),
1810                     $session->param('branchname'),   $session->param('flags'),
1811                     $session->param('emailaddress'), $session->param('shibboleth'),
1812                     $session->param('desk_id'),      $session->param('desk_name'),
1813                     $session->param('register_id'),  $session->param('register_name')
1814                 );
1815                 return ( "additional-auth-needed", $session )
1816                     if $session->param('waiting-for-2FA');
1817
1818                 return ( "ok", $session );
1819             } else {
1820                 $session->delete();
1821                 $session->flush;
1822                 return ( "failed", undef );
1823             }
1824
1825         } else {
1826             C4::Context->_new_userenv($sessionID);
1827             C4::Context->interface($session->param('interface'));
1828             C4::Context->set_userenv( undef, q{} );
1829             return ( "anon", $session );
1830         }
1831     } else {
1832         return ( "expired", undef );
1833     }
1834 }
1835
1836 =head2 get_session
1837
1838   use CGI::Session;
1839   my $session = get_session($sessionID);
1840
1841 Given a session ID, retrieve the CGI::Session object used to store
1842 the session's state.  The session object can be used to store
1843 data that needs to be accessed by different scripts during a
1844 user's session.
1845
1846 If the C<$sessionID> parameter is an empty string, a new session
1847 will be created.
1848
1849 =cut
1850
1851 sub _get_session_params {
1852     my $storage_method = C4::Context->preference('SessionStorage');
1853     if ( $storage_method eq 'mysql' ) {
1854         my $dbh = C4::Context->dbh;
1855         return { dsn => "serializer:yamlxs;driver:MySQL;id:md5", dsn_args => { Handle => $dbh } };
1856     }
1857     elsif ( $storage_method eq 'Pg' ) {
1858         my $dbh = C4::Context->dbh;
1859         return { dsn => "serializer:yamlxs;driver:PostgreSQL;id:md5", dsn_args => { Handle => $dbh } };
1860     }
1861     elsif ( $storage_method eq 'memcached' && Koha::Caches->get_instance->memcached_cache ) {
1862         my $memcached = Koha::Caches->get_instance()->memcached_cache;
1863         return { dsn => "serializer:yamlxs;driver:memcached;id:md5", dsn_args => { Memcached => $memcached } };
1864     }
1865     else {
1866         # catch all defaults to tmp should work on all systems
1867         my $dir = C4::Context::temporary_directory;
1868         my $instance = C4::Context->config( 'database' ); #actually for packages not exactly the instance name, but generally safer to leave it as it is
1869         return { dsn => "serializer:yamlxs;driver:File;id:md5", dsn_args => { Directory => "$dir/cgisess_$instance" } };
1870     }
1871 }
1872
1873 sub get_session {
1874     my $sessionID      = shift;
1875     my $params = _get_session_params();
1876     my $session;
1877     if( $sessionID ) { # find existing
1878         CGI::Session::ErrorHandler->set_error( q{} ); # clear error, cpan issue #111463
1879         $session = CGI::Session->load( $params->{dsn}, $sessionID, $params->{dsn_args} );
1880     } else {
1881         $session = CGI::Session->new( $params->{dsn}, $sessionID, $params->{dsn_args} );
1882         # no need to flush here
1883     }
1884     return $session;
1885 }
1886
1887
1888 # FIXME no_set_userenv may be replaced with force_branchcode_for_userenv
1889 # (or something similar)
1890 # Currently it's only passed from C4::SIP::ILS::Patron::check_password, but
1891 # not having a userenv defined could cause a crash.
1892 sub checkpw {
1893     my ( $userid, $password, $query, $type, $no_set_userenv ) = @_;
1894     $type = 'opac' unless $type;
1895
1896     # Get shibboleth login attribute
1897     my $shib = C4::Context->config('useshibboleth') && shib_ok();
1898     my $shib_login = $shib ? get_login_shib() : undef;
1899
1900     my @return;
1901     my $patron;
1902     if ( defined $userid ){
1903         $patron = Koha::Patrons->find({ userid => $userid });
1904         $patron = Koha::Patrons->find({ cardnumber => $userid }) unless $patron;
1905     }
1906     my $check_internal_as_fallback = 0;
1907     my $passwd_ok = 0;
1908     # Note: checkpw_* routines returns:
1909     # 1 if auth is ok
1910     # 0 if auth is nok
1911     # -1 if user bind failed (LDAP only)
1912
1913     if ( $patron and ( $patron->account_locked )  ) {
1914         # Nothing to check, account is locked
1915     } elsif ($ldap && defined($password)) {
1916         my ( $retval, $retcard, $retuserid ) = checkpw_ldap(@_);    # EXTERNAL AUTH
1917         if ( $retval == 1 ) {
1918             @return = ( $retval, $retcard, $retuserid );
1919             $passwd_ok = 1;
1920         }
1921         $check_internal_as_fallback = 1 if $retval == 0;
1922
1923     } elsif ( $cas && $query && $query->param('ticket') ) {
1924
1925         # In case of a CAS authentication, we use the ticket instead of the password
1926         my $ticket = $query->param('ticket');
1927         $query->delete('ticket');                                   # remove ticket to come back to original URL
1928         my ( $retval, $retcard, $retuserid, $cas_ticket ) = checkpw_cas( $ticket, $query, $type );    # EXTERNAL AUTH
1929         if ( $retval ) {
1930             @return = ( $retval, $retcard, $retuserid, $cas_ticket );
1931         } else {
1932             @return = (0);
1933         }
1934         $passwd_ok = $retval;
1935     }
1936
1937     # If we are in a shibboleth session (shibboleth is enabled, and a shibboleth match attribute is present)
1938     # Check for password to asertain whether we want to be testing against shibboleth or another method this
1939     # time around.
1940     elsif ( $shib && $shib_login && !$password ) {
1941
1942         # In case of a Shibboleth authentication, we expect a shibboleth user attribute
1943         # (defined under shibboleth mapping in koha-conf.xml) to contain the login of the
1944         # shibboleth-authenticated user
1945
1946         # Then, we check if it matches a valid koha user
1947         if ($shib_login) {
1948             my ( $retval, $retcard, $retuserid ) = C4::Auth_with_shibboleth::checkpw_shib($shib_login);    # EXTERNAL AUTH
1949             if ( $retval ) {
1950                 @return = ( $retval, $retcard, $retuserid );
1951             }
1952             $passwd_ok = $retval;
1953         }
1954     } else {
1955         $check_internal_as_fallback = 1;
1956     }
1957
1958     # INTERNAL AUTH
1959     if ( $check_internal_as_fallback ) {
1960         @return = checkpw_internal( $userid, $password, $no_set_userenv);
1961         $passwd_ok = 1 if $return[0] > 0; # 1 or 2
1962     }
1963
1964     if( $patron ) {
1965         if ( $passwd_ok ) {
1966             $patron->update({ login_attempts => 0 });
1967             if( $patron->password_expired ){
1968                 @return = (-2);
1969             }
1970         } elsif( !$patron->account_locked ) {
1971             $patron->update({ login_attempts => $patron->login_attempts + 1 });
1972         }
1973     }
1974
1975     # Optionally log success or failure
1976     if( $patron && $passwd_ok && C4::Context->preference('AuthSuccessLog') ) {
1977         logaction( 'AUTH', 'SUCCESS', $patron->id, "Valid password for $userid", $type );
1978     } elsif( !$passwd_ok && C4::Context->preference('AuthFailureLog') ) {
1979         logaction( 'AUTH', 'FAILURE', $patron ? $patron->id : 0, "Wrong password for $userid", $type );
1980     }
1981
1982     return @return;
1983 }
1984
1985 sub checkpw_internal {
1986     my ( $userid, $password, $no_set_userenv ) = @_;
1987
1988     $password = Encode::encode( 'UTF-8', $password )
1989       if Encode::is_utf8($password);
1990
1991     my $dbh = C4::Context->dbh;
1992     my $sth =
1993       $dbh->prepare(
1994         "select password,cardnumber,borrowernumber,userid,firstname,surname,borrowers.branchcode,branches.branchname,flags from borrowers join branches on borrowers.branchcode=branches.branchcode where userid=?"
1995       );
1996     $sth->execute($userid);
1997     if ( $sth->rows ) {
1998         my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
1999             $surname, $branchcode, $branchname, $flags )
2000           = $sth->fetchrow;
2001
2002         if ( checkpw_hash( $password, $stored_hash ) ) {
2003
2004             C4::Context->set_userenv( "$borrowernumber", $userid, $cardnumber,
2005                 $firstname, $surname, $branchcode, $branchname, $flags ) unless $no_set_userenv;
2006             return 1, $cardnumber, $userid;
2007         }
2008     }
2009     $sth =
2010       $dbh->prepare(
2011         "select password,cardnumber,borrowernumber,userid,firstname,surname,borrowers.branchcode,branches.branchname,flags from borrowers join branches on borrowers.branchcode=branches.branchcode where cardnumber=?"
2012       );
2013     $sth->execute($userid);
2014     if ( $sth->rows ) {
2015         my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
2016             $surname, $branchcode, $branchname, $flags )
2017           = $sth->fetchrow;
2018
2019         if ( checkpw_hash( $password, $stored_hash ) ) {
2020
2021             C4::Context->set_userenv( $borrowernumber, $userid, $cardnumber,
2022                 $firstname, $surname, $branchcode, $branchname, $flags ) unless $no_set_userenv;
2023             return 1, $cardnumber, $userid;
2024         }
2025     }
2026     return 0;
2027 }
2028
2029 sub checkpw_hash {
2030     my ( $password, $stored_hash ) = @_;
2031
2032     return if $stored_hash eq '!';
2033
2034     # check what encryption algorithm was implemented: Bcrypt - if the hash starts with '$2' it is Bcrypt else md5
2035     my $hash;
2036     if ( substr( $stored_hash, 0, 2 ) eq '$2' ) {
2037         $hash = hash_password( $password, $stored_hash );
2038     } else {
2039         $hash = md5_base64($password);
2040     }
2041     return $hash eq $stored_hash;
2042 }
2043
2044 =head2 getuserflags
2045
2046     my $authflags = getuserflags($flags, $userid, [$dbh]);
2047
2048 Translates integer flags into permissions strings hash.
2049
2050 C<$flags> is the integer userflags value ( borrowers.userflags )
2051 C<$userid> is the members.userid, used for building subpermissions
2052 C<$authflags> is a hashref of permissions
2053
2054 =cut
2055
2056 sub getuserflags {
2057     my $flags  = shift;
2058     my $userid = shift;
2059     my $dbh    = @_ ? shift : C4::Context->dbh;
2060     my $userflags;
2061     {
2062         # I don't want to do this, but if someone logs in as the database
2063         # user, it would be preferable not to spam them to death with
2064         # numeric warnings. So, we make $flags numeric.
2065         no warnings 'numeric';
2066         $flags += 0;
2067     }
2068     my $sth = $dbh->prepare("SELECT bit, flag, defaulton FROM userflags");
2069     $sth->execute;
2070
2071     while ( my ( $bit, $flag, $defaulton ) = $sth->fetchrow ) {
2072         if ( ( $flags & ( 2**$bit ) ) || $defaulton ) {
2073             $userflags->{$flag} = 1;
2074         }
2075         else {
2076             $userflags->{$flag} = 0;
2077         }
2078     }
2079
2080     # get subpermissions and merge with top-level permissions
2081     my $user_subperms = get_user_subpermissions($userid);
2082     foreach my $module ( keys %$user_subperms ) {
2083         next if $userflags->{$module} == 1;    # user already has permission for everything in this module
2084         $userflags->{$module} = $user_subperms->{$module};
2085     }
2086
2087     return $userflags;
2088 }
2089
2090 =head2 get_user_subpermissions
2091
2092   $user_perm_hashref = get_user_subpermissions($userid);
2093
2094 Given the userid (note, not the borrowernumber) of a staff user,
2095 return a hashref of hashrefs of the specific subpermissions
2096 accorded to the user.  An example return is
2097
2098  {
2099     tools => {
2100         export_catalog => 1,
2101         import_patrons => 1,
2102     }
2103  }
2104
2105 The top-level hash-key is a module or function code from
2106 userflags.flag, while the second-level key is a code
2107 from permissions.
2108
2109 The results of this function do not give a complete picture
2110 of the functions that a staff user can access; it is also
2111 necessary to check borrowers.flags.
2112
2113 =cut
2114
2115 sub get_user_subpermissions {
2116     my $userid = shift;
2117
2118     my $dbh = C4::Context->dbh;
2119     my $sth = $dbh->prepare( "SELECT flag, user_permissions.code
2120                              FROM user_permissions
2121                              JOIN permissions USING (module_bit, code)
2122                              JOIN userflags ON (module_bit = bit)
2123                              JOIN borrowers USING (borrowernumber)
2124                              WHERE userid = ?" );
2125     $sth->execute($userid);
2126
2127     my $user_perms = {};
2128     while ( my $perm = $sth->fetchrow_hashref ) {
2129         $user_perms->{ $perm->{'flag'} }->{ $perm->{'code'} } = 1;
2130     }
2131     return $user_perms;
2132 }
2133
2134 =head2 get_all_subpermissions
2135
2136   my $perm_hashref = get_all_subpermissions();
2137
2138 Returns a hashref of hashrefs defining all specific
2139 permissions currently defined.  The return value
2140 has the same structure as that of C<get_user_subpermissions>,
2141 except that the innermost hash value is the description
2142 of the subpermission.
2143
2144 =cut
2145
2146 sub get_all_subpermissions {
2147     my $dbh = C4::Context->dbh;
2148     my $sth = $dbh->prepare( "SELECT flag, code
2149                              FROM permissions
2150                              JOIN userflags ON (module_bit = bit)" );
2151     $sth->execute();
2152
2153     my $all_perms = {};
2154     while ( my $perm = $sth->fetchrow_hashref ) {
2155         $all_perms->{ $perm->{'flag'} }->{ $perm->{'code'} } = 1;
2156     }
2157     return $all_perms;
2158 }
2159
2160 =head2 haspermission
2161
2162   $flagsrequired = '*';                                 # Any permission at all
2163   $flagsrequired = 'a_flag';                            # a_flag must be satisfied (all subpermissions)
2164   $flagsrequired = [ 'a_flag', 'b_flag' ];              # a_flag OR b_flag must be satisfied
2165   $flagsrequired = { 'a_flag => 1, 'b_flag' => 1 };     # a_flag AND b_flag must be satisfied
2166   $flagsrequired = { 'a_flag' => 'sub_a' };             # sub_a of a_flag must be satisfied
2167   $flagsrequired = { 'a_flag' => [ 'sub_a, 'sub_b' ] }; # sub_a OR sub_b of a_flag must be satisfied
2168
2169   $flags = ($userid, $flagsrequired);
2170
2171 C<$userid> the userid of the member
2172 C<$flags> is a query structure similar to that used by SQL::Abstract that
2173 denotes the combination of flags required. It is a required parameter.
2174
2175 The main logic of this method is that things in arrays are OR'ed, and things
2176 in hashes are AND'ed. The `*` character can be used, at any depth, to denote `ANY`
2177
2178 Returns member's flags or 0 if a permission is not met.
2179
2180 =cut
2181
2182 sub _dispatch {
2183     my ($required, $flags) = @_;
2184
2185     my $ref = ref($required);
2186     if ($ref eq '') {
2187         if ($required eq '*') {
2188             return 0 unless ( $flags or ref( $flags ) );
2189         } else {
2190             return 0 unless ( $flags and (!ref( $flags ) || $flags->{$required} ));
2191         }
2192     } elsif ($ref eq 'HASH') {
2193         foreach my $key (keys %{$required}) {
2194             next if $flags == 1;
2195             my $require = $required->{$key};
2196             my $rflags  = $flags->{$key};
2197             return 0 unless _dispatch($require, $rflags);
2198         }
2199     } elsif ($ref eq 'ARRAY') {
2200         my $satisfied = 0;
2201         foreach my $require ( @{$required} ) {
2202             my $rflags =
2203               ( ref($flags) && !ref($require) && ( $require ne '*' ) )
2204               ? $flags->{$require}
2205               : $flags;
2206             $satisfied++ if _dispatch( $require, $rflags );
2207         }
2208         return 0 unless $satisfied;
2209     } else {
2210         croak "Unexpected structure found: $ref";
2211     }
2212
2213     return $flags;
2214 };
2215
2216 sub haspermission {
2217     my ( $userid, $flagsrequired ) = @_;
2218
2219     #Koha::Exceptions::WrongParameter->throw('$flagsrequired should not be undef')
2220     #  unless defined($flagsrequired);
2221
2222     my $sth = C4::Context->dbh->prepare("SELECT flags FROM borrowers WHERE userid=?");
2223     $sth->execute($userid);
2224     my $row = $sth->fetchrow();
2225     my $flags = getuserflags( $row, $userid );
2226
2227     return $flags unless defined($flagsrequired);
2228     return $flags if $flags->{superlibrarian};
2229     return _dispatch($flagsrequired, $flags);
2230
2231     #FIXME - This fcn should return the failed permission so a suitable error msg can be delivered.
2232 }
2233
2234 =head2 in_iprange
2235
2236   $flags = ($iprange);
2237
2238 C<$iprange> A space separated string describing an IP range. Can include single IPs or ranges
2239
2240 Returns 1 if the remote address is in the provided iprange, or 0 otherwise.
2241
2242 =cut
2243
2244 sub in_iprange {
2245     my ($iprange) = @_;
2246     my $result = 1;
2247     my @allowedipranges = $iprange ? split(' ', $iprange) : ();
2248     if (scalar @allowedipranges > 0) {
2249         my @rangelist;
2250         eval { @rangelist = Net::CIDR::range2cidr(@allowedipranges); }; return 0 if $@;
2251         eval { $result = Net::CIDR::cidrlookup($ENV{'REMOTE_ADDR'}, @rangelist) } || Koha::Logger->get->warn('cidrlookup failed for ' . join(' ',@rangelist) );
2252      }
2253      return $result ? 1 : 0;
2254 }
2255
2256 sub getborrowernumber {
2257     my ($userid) = @_;
2258     my $userenv = C4::Context->userenv;
2259     if ( defined($userenv) && ref($userenv) eq 'HASH' && $userenv->{number} ) {
2260         return $userenv->{number};
2261     }
2262     my $dbh = C4::Context->dbh;
2263     for my $field ( 'userid', 'cardnumber' ) {
2264         my $sth =
2265           $dbh->prepare("select borrowernumber from borrowers where $field=?");
2266         $sth->execute($userid);
2267         if ( $sth->rows ) {
2268             my ($bnumber) = $sth->fetchrow;
2269             return $bnumber;
2270         }
2271     }
2272     return 0;
2273 }
2274
2275 =head2 track_login_daily
2276
2277     track_login_daily( $userid );
2278
2279 Wraps the call to $patron->track_login, the method used to update borrowers.lastseen. We only call track_login once a day.
2280
2281 =cut
2282
2283 sub track_login_daily {
2284     my $userid = shift;
2285     return if !$userid || !C4::Context->preference('TrackLastPatronActivity');
2286
2287     my $cache     = Koha::Caches->get_instance();
2288     my $cache_key = "track_login_" . $userid;
2289     my $cached    = $cache->get_from_cache($cache_key);
2290     my $today = dt_from_string()->ymd;
2291     return if $cached && $cached eq $today;
2292
2293     my $patron = Koha::Patrons->find({ userid => $userid });
2294     return unless $patron;
2295     $patron->track_login;
2296     $cache->set_in_cache( $cache_key, $today );
2297 }
2298
2299 END { }    # module clean-up code here (global destructor)
2300 1;
2301 __END__
2302
2303 =head1 SEE ALSO
2304
2305 CGI(3)
2306
2307 C4::Output(3)
2308
2309 Crypt::Eksblowfish::Bcrypt(3)
2310
2311 Digest::MD5(3)
2312
2313 =cut