Bug 29914: Make check_cookie_auth compare the userid
[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 "anon" -- user not authenticated but valid for anonymous session.
1645
1646 =item "failed" -- credentials are not correct; C<$sessionid> are undef
1647
1648 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1649
1650 =item "expired -- session cookie has expired; API user should resubmit userid and password
1651
1652 =item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1653
1654 =back
1655
1656 =cut
1657
1658 sub check_cookie_auth {
1659     my $sessionID     = shift;
1660     my $flagsrequired = shift;
1661     my $params        = shift;
1662
1663     my $remote_addr = $params->{remote_addr} || $ENV{REMOTE_ADDR};
1664
1665     my $skip_version_check = $params->{skip_version_check}; # Only for checkauth
1666
1667     unless ( $skip_version_check ) {
1668         unless ( C4::Context->preference('Version') ) {
1669
1670             # database has not been installed yet
1671             return ( "maintenance", undef );
1672         }
1673         my $kohaversion = Koha::version();
1674         $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1675         if ( C4::Context->preference('Version') < $kohaversion ) {
1676
1677             # database in need of version update; assume that
1678             # no API should be called while databsae is in
1679             # this condition.
1680             return ( "maintenance", undef );
1681         }
1682     }
1683
1684     # see if we have a valid session cookie already
1685     # however, if a userid parameter is present (i.e., from
1686     # a form submission, assume that any current cookie
1687     # is to be ignored
1688     unless ( defined $sessionID and $sessionID ) {
1689         return ( "failed", undef );
1690     }
1691     my $session   = get_session($sessionID);
1692     C4::Context->_new_userenv($sessionID);
1693     if ($session) {
1694         C4::Context->interface($session->param('interface'));
1695         C4::Context->set_userenv(
1696             $session->param('number'),       $session->param('id') // '',
1697             $session->param('cardnumber'),   $session->param('firstname'),
1698             $session->param('surname'),      $session->param('branch'),
1699             $session->param('branchname'),   $session->param('flags'),
1700             $session->param('emailaddress'), $session->param('shibboleth'),
1701             $session->param('desk_id'),      $session->param('desk_name'),
1702             $session->param('register_id'),  $session->param('register_name')
1703         );
1704
1705         my $userid   = $session->param('id');
1706         my $ip       = $session->param('ip');
1707         my $lasttime = $session->param('lasttime');
1708         my $timeout = _timeout_syspref();
1709
1710         if ( !$lasttime || ( $lasttime < time() - $timeout ) ) {
1711
1712             # time out
1713             $session->delete();
1714             $session->flush;
1715             C4::Context->_unset_userenv($sessionID);
1716             return ("expired", undef);
1717         } elsif ( C4::Context->preference('SessionRestrictionByIP') && $ip ne $remote_addr ) {
1718
1719             # IP address changed
1720             $session->delete();
1721             $session->flush;
1722             C4::Context->_unset_userenv($sessionID);
1723             return ( "restricted", undef, { old_ip => $ip, new_ip => $remote_addr});
1724         } elsif ( $userid ) {
1725             $session->param( 'lasttime', time() );
1726             my $flags = defined($flagsrequired) ? haspermission( $userid, $flagsrequired ) : 1;
1727             if ($flags) {
1728                 return ( "ok", $session );
1729             }
1730         } else {
1731             return ( "anon", $session );
1732         }
1733         $session->delete();
1734         $session->flush;
1735         C4::Context->_unset_userenv($sessionID);
1736         return ( "failed", undef );
1737     } else {
1738         return ( "expired", undef );
1739     }
1740 }
1741
1742 =head2 get_session
1743
1744   use CGI::Session;
1745   my $session = get_session($sessionID);
1746
1747 Given a session ID, retrieve the CGI::Session object used to store
1748 the session's state.  The session object can be used to store
1749 data that needs to be accessed by different scripts during a
1750 user's session.
1751
1752 If the C<$sessionID> parameter is an empty string, a new session
1753 will be created.
1754
1755 =cut
1756
1757 sub _get_session_params {
1758     my $storage_method = C4::Context->preference('SessionStorage');
1759     if ( $storage_method eq 'mysql' ) {
1760         my $dbh = C4::Context->dbh;
1761         return { dsn => "serializer:yamlxs;driver:MySQL;id:md5", dsn_args => { Handle => $dbh } };
1762     }
1763     elsif ( $storage_method eq 'Pg' ) {
1764         my $dbh = C4::Context->dbh;
1765         return { dsn => "serializer:yamlxs;driver:PostgreSQL;id:md5", dsn_args => { Handle => $dbh } };
1766     }
1767     elsif ( $storage_method eq 'memcached' && Koha::Caches->get_instance->memcached_cache ) {
1768         my $memcached = Koha::Caches->get_instance()->memcached_cache;
1769         return { dsn => "serializer:yamlxs;driver:memcached;id:md5", dsn_args => { Memcached => $memcached } };
1770     }
1771     else {
1772         # catch all defaults to tmp should work on all systems
1773         my $dir = C4::Context::temporary_directory;
1774         my $instance = C4::Context->config( 'database' ); #actually for packages not exactly the instance name, but generally safer to leave it as it is
1775         return { dsn => "serializer:yamlxs;driver:File;id:md5", dsn_args => { Directory => "$dir/cgisess_$instance" } };
1776     }
1777 }
1778
1779 sub get_session {
1780     my $sessionID      = shift;
1781     my $params = _get_session_params();
1782     my $session = CGI::Session->new( $params->{dsn}, $sessionID, $params->{dsn_args} );
1783     if ( ! $session ){
1784         die CGI::Session->errstr();
1785     }
1786     return $session;
1787 }
1788
1789
1790 # FIXME no_set_userenv may be replaced with force_branchcode_for_userenv
1791 # (or something similar)
1792 # Currently it's only passed from C4::SIP::ILS::Patron::check_password, but
1793 # not having a userenv defined could cause a crash.
1794 sub checkpw {
1795     my ( $dbh, $userid, $password, $query, $type, $no_set_userenv ) = @_;
1796     $type = 'opac' unless $type;
1797
1798     # Get shibboleth login attribute
1799     my $shib = C4::Context->config('useshibboleth') && shib_ok();
1800     my $shib_login = $shib ? get_login_shib() : undef;
1801
1802     my @return;
1803     my $patron;
1804     if ( defined $userid ){
1805         $patron = Koha::Patrons->find({ userid => $userid });
1806         $patron = Koha::Patrons->find({ cardnumber => $userid }) unless $patron;
1807     }
1808     my $check_internal_as_fallback = 0;
1809     my $passwd_ok = 0;
1810     # Note: checkpw_* routines returns:
1811     # 1 if auth is ok
1812     # 0 if auth is nok
1813     # -1 if user bind failed (LDAP only)
1814
1815     if ( $patron and $patron->account_locked ) {
1816         # Nothing to check, account is locked
1817     } elsif ($ldap && defined($password)) {
1818         my ( $retval, $retcard, $retuserid ) = checkpw_ldap(@_);    # EXTERNAL AUTH
1819         if ( $retval == 1 ) {
1820             @return = ( $retval, $retcard, $retuserid );
1821             $passwd_ok = 1;
1822         }
1823         $check_internal_as_fallback = 1 if $retval == 0;
1824
1825     } elsif ( $cas && $query && $query->param('ticket') ) {
1826
1827         # In case of a CAS authentication, we use the ticket instead of the password
1828         my $ticket = $query->param('ticket');
1829         $query->delete('ticket');                                   # remove ticket to come back to original URL
1830         my ( $retval, $retcard, $retuserid, $cas_ticket ) = checkpw_cas( $dbh, $ticket, $query, $type );    # EXTERNAL AUTH
1831         if ( $retval ) {
1832             @return = ( $retval, $retcard, $retuserid, $cas_ticket );
1833         } else {
1834             @return = (0);
1835         }
1836         $passwd_ok = $retval;
1837     }
1838
1839     # If we are in a shibboleth session (shibboleth is enabled, and a shibboleth match attribute is present)
1840     # Check for password to asertain whether we want to be testing against shibboleth or another method this
1841     # time around.
1842     elsif ( $shib && $shib_login && !$password ) {
1843
1844         # In case of a Shibboleth authentication, we expect a shibboleth user attribute
1845         # (defined under shibboleth mapping in koha-conf.xml) to contain the login of the
1846         # shibboleth-authenticated user
1847
1848         # Then, we check if it matches a valid koha user
1849         if ($shib_login) {
1850             my ( $retval, $retcard, $retuserid ) = C4::Auth_with_shibboleth::checkpw_shib($shib_login);    # EXTERNAL AUTH
1851             if ( $retval ) {
1852                 @return = ( $retval, $retcard, $retuserid );
1853             }
1854             $passwd_ok = $retval;
1855         }
1856     } else {
1857         $check_internal_as_fallback = 1;
1858     }
1859
1860     # INTERNAL AUTH
1861     if ( $check_internal_as_fallback ) {
1862         @return = checkpw_internal( $dbh, $userid, $password, $no_set_userenv);
1863         $passwd_ok = 1 if $return[0] > 0; # 1 or 2
1864     }
1865
1866     if( $patron ) {
1867         if ( $passwd_ok ) {
1868             $patron->update({ login_attempts => 0 });
1869         } elsif( !$patron->account_locked ) {
1870             $patron->update({ login_attempts => $patron->login_attempts + 1 });
1871         }
1872     }
1873
1874     # Optionally log success or failure
1875     if( $patron && $passwd_ok && C4::Context->preference('AuthSuccessLog') ) {
1876         logaction( 'AUTH', 'SUCCESS', $patron->id, "Valid password for $userid", $type );
1877     } elsif( !$passwd_ok && C4::Context->preference('AuthFailureLog') ) {
1878         logaction( 'AUTH', 'FAILURE', $patron ? $patron->id : 0, "Wrong password for $userid", $type );
1879     }
1880
1881     return @return;
1882 }
1883
1884 sub checkpw_internal {
1885     my ( $dbh, $userid, $password, $no_set_userenv ) = @_;
1886
1887     $password = Encode::encode( 'UTF-8', $password )
1888       if Encode::is_utf8($password);
1889
1890     my $sth =
1891       $dbh->prepare(
1892         "select password,cardnumber,borrowernumber,userid,firstname,surname,borrowers.branchcode,branches.branchname,flags from borrowers join branches on borrowers.branchcode=branches.branchcode where userid=?"
1893       );
1894     $sth->execute($userid);
1895     if ( $sth->rows ) {
1896         my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
1897             $surname, $branchcode, $branchname, $flags )
1898           = $sth->fetchrow;
1899
1900         if ( checkpw_hash( $password, $stored_hash ) ) {
1901
1902             C4::Context->set_userenv( "$borrowernumber", $userid, $cardnumber,
1903                 $firstname, $surname, $branchcode, $branchname, $flags ) unless $no_set_userenv;
1904             return 1, $cardnumber, $userid;
1905         }
1906     }
1907     $sth =
1908       $dbh->prepare(
1909         "select password,cardnumber,borrowernumber,userid,firstname,surname,borrowers.branchcode,branches.branchname,flags from borrowers join branches on borrowers.branchcode=branches.branchcode where cardnumber=?"
1910       );
1911     $sth->execute($userid);
1912     if ( $sth->rows ) {
1913         my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
1914             $surname, $branchcode, $branchname, $flags )
1915           = $sth->fetchrow;
1916
1917         if ( checkpw_hash( $password, $stored_hash ) ) {
1918
1919             C4::Context->set_userenv( $borrowernumber, $userid, $cardnumber,
1920                 $firstname, $surname, $branchcode, $branchname, $flags ) unless $no_set_userenv;
1921             return 1, $cardnumber, $userid;
1922         }
1923     }
1924     return 0;
1925 }
1926
1927 sub checkpw_hash {
1928     my ( $password, $stored_hash ) = @_;
1929
1930     return if $stored_hash eq '!';
1931
1932     # check what encryption algorithm was implemented: Bcrypt - if the hash starts with '$2' it is Bcrypt else md5
1933     my $hash;
1934     if ( substr( $stored_hash, 0, 2 ) eq '$2' ) {
1935         $hash = hash_password( $password, $stored_hash );
1936     } else {
1937         $hash = md5_base64($password);
1938     }
1939     return $hash eq $stored_hash;
1940 }
1941
1942 =head2 getuserflags
1943
1944     my $authflags = getuserflags($flags, $userid, [$dbh]);
1945
1946 Translates integer flags into permissions strings hash.
1947
1948 C<$flags> is the integer userflags value ( borrowers.userflags )
1949 C<$userid> is the members.userid, used for building subpermissions
1950 C<$authflags> is a hashref of permissions
1951
1952 =cut
1953
1954 sub getuserflags {
1955     my $flags  = shift;
1956     my $userid = shift;
1957     my $dbh    = @_ ? shift : C4::Context->dbh;
1958     my $userflags;
1959     {
1960         # I don't want to do this, but if someone logs in as the database
1961         # user, it would be preferable not to spam them to death with
1962         # numeric warnings. So, we make $flags numeric.
1963         no warnings 'numeric';
1964         $flags += 0;
1965     }
1966     my $sth = $dbh->prepare("SELECT bit, flag, defaulton FROM userflags");
1967     $sth->execute;
1968
1969     while ( my ( $bit, $flag, $defaulton ) = $sth->fetchrow ) {
1970         if ( ( $flags & ( 2**$bit ) ) || $defaulton ) {
1971             $userflags->{$flag} = 1;
1972         }
1973         else {
1974             $userflags->{$flag} = 0;
1975         }
1976     }
1977
1978     # get subpermissions and merge with top-level permissions
1979     my $user_subperms = get_user_subpermissions($userid);
1980     foreach my $module ( keys %$user_subperms ) {
1981         next if $userflags->{$module} == 1;    # user already has permission for everything in this module
1982         $userflags->{$module} = $user_subperms->{$module};
1983     }
1984
1985     return $userflags;
1986 }
1987
1988 =head2 get_user_subpermissions
1989
1990   $user_perm_hashref = get_user_subpermissions($userid);
1991
1992 Given the userid (note, not the borrowernumber) of a staff user,
1993 return a hashref of hashrefs of the specific subpermissions
1994 accorded to the user.  An example return is
1995
1996  {
1997     tools => {
1998         export_catalog => 1,
1999         import_patrons => 1,
2000     }
2001  }
2002
2003 The top-level hash-key is a module or function code from
2004 userflags.flag, while the second-level key is a code
2005 from permissions.
2006
2007 The results of this function do not give a complete picture
2008 of the functions that a staff user can access; it is also
2009 necessary to check borrowers.flags.
2010
2011 =cut
2012
2013 sub get_user_subpermissions {
2014     my $userid = shift;
2015
2016     my $dbh = C4::Context->dbh;
2017     my $sth = $dbh->prepare( "SELECT flag, user_permissions.code
2018                              FROM user_permissions
2019                              JOIN permissions USING (module_bit, code)
2020                              JOIN userflags ON (module_bit = bit)
2021                              JOIN borrowers USING (borrowernumber)
2022                              WHERE userid = ?" );
2023     $sth->execute($userid);
2024
2025     my $user_perms = {};
2026     while ( my $perm = $sth->fetchrow_hashref ) {
2027         $user_perms->{ $perm->{'flag'} }->{ $perm->{'code'} } = 1;
2028     }
2029     return $user_perms;
2030 }
2031
2032 =head2 get_all_subpermissions
2033
2034   my $perm_hashref = get_all_subpermissions();
2035
2036 Returns a hashref of hashrefs defining all specific
2037 permissions currently defined.  The return value
2038 has the same structure as that of C<get_user_subpermissions>,
2039 except that the innermost hash value is the description
2040 of the subpermission.
2041
2042 =cut
2043
2044 sub get_all_subpermissions {
2045     my $dbh = C4::Context->dbh;
2046     my $sth = $dbh->prepare( "SELECT flag, code
2047                              FROM permissions
2048                              JOIN userflags ON (module_bit = bit)" );
2049     $sth->execute();
2050
2051     my $all_perms = {};
2052     while ( my $perm = $sth->fetchrow_hashref ) {
2053         $all_perms->{ $perm->{'flag'} }->{ $perm->{'code'} } = 1;
2054     }
2055     return $all_perms;
2056 }
2057
2058 =head2 haspermission
2059
2060   $flagsrequired = '*';                                 # Any permission at all
2061   $flagsrequired = 'a_flag';                            # a_flag must be satisfied (all subpermissions)
2062   $flagsrequired = [ 'a_flag', 'b_flag' ];              # a_flag OR b_flag must be satisfied
2063   $flagsrequired = { 'a_flag => 1, 'b_flag' => 1 };     # a_flag AND b_flag must be satisfied
2064   $flagsrequired = { 'a_flag' => 'sub_a' };             # sub_a of a_flag must be satisfied
2065   $flagsrequired = { 'a_flag' => [ 'sub_a, 'sub_b' ] }; # sub_a OR sub_b of a_flag must be satisfied
2066
2067   $flags = ($userid, $flagsrequired);
2068
2069 C<$userid> the userid of the member
2070 C<$flags> is a query structure similar to that used by SQL::Abstract that
2071 denotes the combination of flags required. It is a required parameter.
2072
2073 The main logic of this method is that things in arrays are OR'ed, and things
2074 in hashes are AND'ed. The `*` character can be used, at any depth, to denote `ANY`
2075
2076 Returns member's flags or 0 if a permission is not met.
2077
2078 =cut
2079
2080 sub _dispatch {
2081     my ($required, $flags) = @_;
2082
2083     my $ref = ref($required);
2084     if ($ref eq '') {
2085         if ($required eq '*') {
2086             return 0 unless ( $flags or ref( $flags ) );
2087         } else {
2088             return 0 unless ( $flags and (!ref( $flags ) || $flags->{$required} ));
2089         }
2090     } elsif ($ref eq 'HASH') {
2091         foreach my $key (keys %{$required}) {
2092             next if $flags == 1;
2093             my $require = $required->{$key};
2094             my $rflags  = $flags->{$key};
2095             return 0 unless _dispatch($require, $rflags);
2096         }
2097     } elsif ($ref eq 'ARRAY') {
2098         my $satisfied = 0;
2099         foreach my $require ( @{$required} ) {
2100             my $rflags =
2101               ( ref($flags) && !ref($require) && ( $require ne '*' ) )
2102               ? $flags->{$require}
2103               : $flags;
2104             $satisfied++ if _dispatch( $require, $rflags );
2105         }
2106         return 0 unless $satisfied;
2107     } else {
2108         croak "Unexpected structure found: $ref";
2109     }
2110
2111     return $flags;
2112 };
2113
2114 sub haspermission {
2115     my ( $userid, $flagsrequired ) = @_;
2116
2117     #Koha::Exceptions::WrongParameter->throw('$flagsrequired should not be undef')
2118     #  unless defined($flagsrequired);
2119
2120     my $sth = C4::Context->dbh->prepare("SELECT flags FROM borrowers WHERE userid=?");
2121     $sth->execute($userid);
2122     my $row = $sth->fetchrow();
2123     my $flags = getuserflags( $row, $userid );
2124
2125     return $flags unless defined($flagsrequired);
2126     return $flags if $flags->{superlibrarian};
2127     return _dispatch($flagsrequired, $flags);
2128
2129     #FIXME - This fcn should return the failed permission so a suitable error msg can be delivered.
2130 }
2131
2132 =head2 in_iprange
2133
2134   $flags = ($iprange);
2135
2136 C<$iprange> A space separated string describing an IP range. Can include single IPs or ranges
2137
2138 Returns 1 if the remote address is in the provided iprange, or 0 otherwise.
2139
2140 =cut
2141
2142 sub in_iprange {
2143     my ($iprange) = @_;
2144     my $result = 1;
2145     my @allowedipranges = $iprange ? split(' ', $iprange) : ();
2146     if (scalar @allowedipranges > 0) {
2147         my @rangelist;
2148         eval { @rangelist = Net::CIDR::range2cidr(@allowedipranges); }; return 0 if $@;
2149         eval { $result = Net::CIDR::cidrlookup($ENV{'REMOTE_ADDR'}, @rangelist) } || Koha::Logger->get->warn('cidrlookup failed for ' . join(' ',@rangelist) );
2150      }
2151      return $result ? 1 : 0;
2152 }
2153
2154 sub getborrowernumber {
2155     my ($userid) = @_;
2156     my $userenv = C4::Context->userenv;
2157     if ( defined($userenv) && ref($userenv) eq 'HASH' && $userenv->{number} ) {
2158         return $userenv->{number};
2159     }
2160     my $dbh = C4::Context->dbh;
2161     for my $field ( 'userid', 'cardnumber' ) {
2162         my $sth =
2163           $dbh->prepare("select borrowernumber from borrowers where $field=?");
2164         $sth->execute($userid);
2165         if ( $sth->rows ) {
2166             my ($bnumber) = $sth->fetchrow;
2167             return $bnumber;
2168         }
2169     }
2170     return 0;
2171 }
2172
2173 =head2 track_login_daily
2174
2175     track_login_daily( $userid );
2176
2177 Wraps the call to $patron->track_login, the method used to update borrowers.lastseen. We only call track_login once a day.
2178
2179 =cut
2180
2181 sub track_login_daily {
2182     my $userid = shift;
2183     return if !$userid || !C4::Context->preference('TrackLastPatronActivity');
2184
2185     my $cache     = Koha::Caches->get_instance();
2186     my $cache_key = "track_login_" . $userid;
2187     my $cached    = $cache->get_from_cache($cache_key);
2188     my $today = dt_from_string()->ymd;
2189     return if $cached && $cached eq $today;
2190
2191     my $patron = Koha::Patrons->find({ userid => $userid });
2192     return unless $patron;
2193     $patron->track_login;
2194     $cache->set_in_cache( $cache_key, $today );
2195 }
2196
2197 END { }    # module clean-up code here (global destructor)
2198 1;
2199 __END__
2200
2201 =head1 SEE ALSO
2202
2203 CGI(3)
2204
2205 C4::Output(3)
2206
2207 Crypt::Eksblowfish::Bcrypt(3)
2208
2209 Digest::MD5(3)
2210
2211 =cut