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