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