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