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