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