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