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