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