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