Bug 25552: Unit tests
[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             # $return: 1 = valid user
1105             if ($return) {
1106
1107                 #_session_log(sprintf "%20s from %16s logged in  at %30s.\n", $userid,$ENV{'REMOTE_ADDR'},(strftime '%c', localtime));
1108                 if ( $flags = haspermission( $userid, $flagsrequired ) ) {
1109                     $loggedin = 1;
1110                 }
1111                 else {
1112                     $info{'nopermission'} = 1;
1113                     C4::Context->_unset_userenv($sessionID);
1114                 }
1115                 my ( $borrowernumber, $firstname, $surname, $userflags,
1116                     $branchcode, $branchname, $emailaddress, $desk_id,
1117                     $desk_name, $register_id, $register_name );
1118
1119                 if ( $return == 1 ) {
1120                     my $select = "
1121                     SELECT borrowernumber, firstname, surname, flags, borrowers.branchcode,
1122                     branches.branchname    as branchname, email
1123                     FROM borrowers
1124                     LEFT JOIN branches on borrowers.branchcode=branches.branchcode
1125                     ";
1126                     my $sth = $dbh->prepare("$select where userid=?");
1127                     $sth->execute($userid);
1128                     unless ( $sth->rows ) {
1129                         $debug and print STDERR "AUTH_1: no rows for userid='$userid'\n";
1130                         $sth = $dbh->prepare("$select where cardnumber=?");
1131                         $sth->execute($cardnumber);
1132
1133                         unless ( $sth->rows ) {
1134                             $debug and print STDERR "AUTH_2a: no rows for cardnumber='$cardnumber'\n";
1135                             $sth->execute($userid);
1136                             unless ( $sth->rows ) {
1137                                 $debug and print STDERR "AUTH_2b: no rows for userid='$userid' AS cardnumber\n";
1138                             }
1139                         }
1140                     }
1141                     if ( $sth->rows ) {
1142                         ( $borrowernumber, $firstname, $surname, $userflags,
1143                             $branchcode, $branchname, $emailaddress ) = $sth->fetchrow;
1144                         $debug and print STDERR "AUTH_3 results: " .
1145                           "$cardnumber,$borrowernumber,$userid,$firstname,$surname,$userflags,$branchcode,$emailaddress\n";
1146                     } else {
1147                         print STDERR "AUTH_3: no results for userid='$userid', cardnumber='$cardnumber'.\n";
1148                     }
1149
1150                     # launch a sequence to check if we have a ip for the branch, i
1151                     # if we have one we replace the branchcode of the userenv by the branch bound in the ip.
1152
1153                     my $ip = $ENV{'REMOTE_ADDR'};
1154
1155                     # if they specify at login, use that
1156                     if ( $query->param('branch') ) {
1157                         $branchcode = $query->param('branch');
1158                         my $library = Koha::Libraries->find($branchcode);
1159                         $branchname = $library? $library->branchname: '';
1160                     }
1161                     if ( $query->param('desk_id') ) {
1162                         $desk_id = $query->param('desk_id');
1163                         my $desk = Koha::Desks->find($desk_id);
1164                         $desk_name = $desk ? $desk->desk_name : '';
1165                     }
1166                     if ( C4::Context->preference('UseCashRegisters') ) {
1167                         my $register =
1168                           $query->param('register_id')
1169                           ? Koha::Cash::Registers->find($query->param('register_id'))
1170                           : Koha::Cash::Registers->search(
1171                             { branch => $branchcode, branch_default => 1 },
1172                             { rows   => 1 } )->single;
1173                         $register_id   = $register->id   if ($register);
1174                         $register_name = $register->name if ($register);
1175                     }
1176                     my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search };
1177                     if ( $type ne 'opac' and C4::Context->boolean_preference('AutoLocation') ) {
1178
1179                         # we have to check they are coming from the right ip range
1180                         my $domain = $branches->{$branchcode}->{'branchip'};
1181                         $domain =~ s|\.\*||g;
1182                         if ( $ip !~ /^$domain/ ) {
1183                             $loggedin = 0;
1184                             $cookie = $query->cookie(
1185                                 -name     => 'CGISESSID',
1186                                 -value    => '',
1187                                 -HttpOnly => 1,
1188                                 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1189                             );
1190                             $info{'wrongip'} = 1;
1191                         }
1192                     }
1193
1194                     foreach my $br ( keys %$branches ) {
1195
1196                         #     now we work with the treatment of ip
1197                         my $domain = $branches->{$br}->{'branchip'};
1198                         if ( $domain && $ip =~ /^$domain/ ) {
1199                             $branchcode = $branches->{$br}->{'branchcode'};
1200
1201                             # new op dev : add the branchname to the cookie
1202                             $branchname    = $branches->{$br}->{'branchname'};
1203                         }
1204                     }
1205                     $session->param( 'number',       $borrowernumber );
1206                     $session->param( 'id',           $userid );
1207                     $session->param( 'cardnumber',   $cardnumber );
1208                     $session->param( 'firstname',    $firstname );
1209                     $session->param( 'surname',      $surname );
1210                     $session->param( 'branch',       $branchcode );
1211                     $session->param( 'branchname',   $branchname );
1212                     $session->param( 'desk_id',      $desk_id);
1213                     $session->param( 'desk_name',     $desk_name);
1214                     $session->param( 'flags',        $userflags );
1215                     $session->param( 'emailaddress', $emailaddress );
1216                     $session->param( 'ip',           $session->remote_addr() );
1217                     $session->param( 'lasttime',     time() );
1218                     $session->param( 'interface',    $type);
1219                     $session->param( 'shibboleth',   $shibSuccess );
1220                     $session->param( 'register_id',  $register_id );
1221                     $session->param( 'register_name',  $register_name );
1222                     $debug and printf STDERR "AUTH_4: (%s)\t%s %s - %s\n", map { $session->param($_) } qw(cardnumber firstname surname branch);
1223                 }
1224                 $session->param('cas_ticket', $cas_ticket) if $cas_ticket;
1225                 C4::Context->set_userenv(
1226                     $session->param('number'),       $session->param('id'),
1227                     $session->param('cardnumber'),   $session->param('firstname'),
1228                     $session->param('surname'),      $session->param('branch'),
1229                     $session->param('branchname'),   $session->param('flags'),
1230                     $session->param('emailaddress'), $session->param('shibboleth'),
1231                     $session->param('desk_id'),      $session->param('desk_name'),
1232                     $session->param('register_id'),  $session->param('register_name')
1233                 );
1234
1235             }
1236             # $return: 0 = invalid user
1237             # reset to anonymous session
1238             else {
1239                 $debug and warn "Login failed, resetting anonymous session...";
1240                 if ($userid) {
1241                     $info{'invalid_username_or_password'} = 1;
1242                     C4::Context->_unset_userenv($sessionID);
1243                 }
1244                 $session->param( 'lasttime', time() );
1245                 $session->param( 'ip',       $session->remote_addr() );
1246                 $session->param( 'sessiontype', 'anon' );
1247                 $session->param( 'interface', $type);
1248             }
1249         }    # END if ( $q_userid
1250         elsif ( $type eq "opac" ) {
1251
1252             # if we are here this is an anonymous session; add public lists to it and a few other items...
1253             # anonymous sessions are created only for the OPAC
1254             $debug and warn "Initiating an anonymous session...";
1255
1256             # setting a couple of other session vars...
1257             $session->param( 'ip',          $session->remote_addr() );
1258             $session->param( 'lasttime',    time() );
1259             $session->param( 'sessiontype', 'anon' );
1260             $session->param( 'interface', $type);
1261         }
1262     }    # END unless ($userid)
1263
1264     # finished authentification, now respond
1265     if ( $loggedin || $authnotrequired )
1266     {
1267         # successful login
1268         unless ($cookie) {
1269             $cookie = $query->cookie(
1270                 -name     => 'CGISESSID',
1271                 -value    => '',
1272                 -HttpOnly => 1,
1273                 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1274             );
1275         }
1276
1277         track_login_daily( $userid );
1278
1279         # In case, that this request was a login attempt, we want to prevent that users can repost the opac login
1280         # request. We therefore redirect the user to the requested page again without the login parameters.
1281         # See Post/Redirect/Get (PRG) design pattern: https://en.wikipedia.org/wiki/Post/Redirect/Get
1282         if ( $type eq "opac" && $query->param('koha_login_context') && $query->param('koha_login_context') ne 'sco' && $query->param('password') && $query->param('userid') ) {
1283             my $uri = URI->new($query->url(-relative=>1, -query_string=>1));
1284             $uri->query_param_delete('userid');
1285             $uri->query_param_delete('password');
1286             $uri->query_param_delete('koha_login_context');
1287             print $query->redirect(-uri => $uri->as_string, -cookie => $cookie, -status=>'303 See other');
1288             exit;
1289         }
1290
1291         return ( $userid, $cookie, $sessionID, $flags );
1292     }
1293
1294     #
1295     #
1296     # AUTH rejected, show the login/password template, after checking the DB.
1297     #
1298     #
1299
1300     # get the inputs from the incoming query
1301     my @inputs = ();
1302     foreach my $name ( param $query) {
1303         (next) if ( $name eq 'userid' || $name eq 'password' || $name eq 'ticket' );
1304         my @value = $query->multi_param($name);
1305         push @inputs, { name => $name, value => $_ } for @value;
1306     }
1307
1308     my $patron = Koha::Patrons->find({ userid => $q_userid }); # Not necessary logged in!
1309
1310     my $LibraryNameTitle = C4::Context->preference("LibraryName");
1311     $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
1312     $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
1313
1314     my $auth_template_name = ( $type eq 'opac' ) ? 'opac-auth.tt' : 'auth.tt';
1315     my $template = C4::Templates::gettemplate( $auth_template_name, $type, $query );
1316     $template->param(
1317         login                                 => 1,
1318         INPUTS                                => \@inputs,
1319         script_name                           => get_script_name(),
1320         casAuthentication                     => C4::Context->preference("casAuthentication"),
1321         shibbolethAuthentication              => $shib,
1322         SessionRestrictionByIP                => C4::Context->preference("SessionRestrictionByIP"),
1323         suggestion                            => C4::Context->preference("suggestion"),
1324         virtualshelves                        => C4::Context->preference("virtualshelves"),
1325         LibraryName                           => "" . C4::Context->preference("LibraryName"),
1326         LibraryNameTitle                      => "" . $LibraryNameTitle,
1327         opacuserlogin                         => C4::Context->preference("opacuserlogin"),
1328         OpacNav                               => C4::Context->preference("OpacNav"),
1329         OpacNavBottom                         => C4::Context->preference("OpacNavBottom"),
1330         OpacFavicon                           => C4::Context->preference("OpacFavicon"),
1331         opacreadinghistory                    => C4::Context->preference("opacreadinghistory"),
1332         opaclanguagesdisplay                  => C4::Context->preference("opaclanguagesdisplay"),
1333         OPACUserJS                            => C4::Context->preference("OPACUserJS"),
1334         opacbookbag                           => "" . C4::Context->preference("opacbookbag"),
1335         OpacCloud                             => C4::Context->preference("OpacCloud"),
1336         OpacTopissue                          => C4::Context->preference("OpacTopissue"),
1337         OpacAuthorities                       => C4::Context->preference("OpacAuthorities"),
1338         OpacBrowser                           => C4::Context->preference("OpacBrowser"),
1339         TagsEnabled                           => C4::Context->preference("TagsEnabled"),
1340         OPACUserCSS                           => C4::Context->preference("OPACUserCSS"),
1341         intranetcolorstylesheet               => C4::Context->preference("intranetcolorstylesheet"),
1342         intranetstylesheet                    => C4::Context->preference("intranetstylesheet"),
1343         intranetbookbag                       => C4::Context->preference("intranetbookbag"),
1344         IntranetNav                           => C4::Context->preference("IntranetNav"),
1345         IntranetFavicon                       => C4::Context->preference("IntranetFavicon"),
1346         IntranetUserCSS                       => C4::Context->preference("IntranetUserCSS"),
1347         IntranetUserJS                        => C4::Context->preference("IntranetUserJS"),
1348         IndependentBranches                   => C4::Context->preference("IndependentBranches"),
1349         AutoLocation                          => C4::Context->preference("AutoLocation"),
1350         wrongip                               => $info{'wrongip'},
1351         PatronSelfRegistration                => C4::Context->preference("PatronSelfRegistration"),
1352         PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"),
1353         opac_css_override                     => $ENV{'OPAC_CSS_OVERRIDE'},
1354         too_many_login_attempts               => ( $patron and $patron->account_locked )
1355     );
1356
1357     $template->param( SCO_login => 1 ) if ( $query->param('sco_user_login') );
1358     $template->param( SCI_login => 1 ) if ( $query->param('sci_user_login') );
1359     $template->param( OpacPublic => C4::Context->preference("OpacPublic") );
1360     $template->param( loginprompt => 1 ) unless $info{'nopermission'};
1361
1362     if ( $type eq 'opac' ) {
1363         require Koha::Virtualshelves;
1364         my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
1365             {
1366                 category       => 2,
1367             }
1368         );
1369         $template->param(
1370             some_public_shelves  => $some_public_shelves,
1371         );
1372     }
1373
1374     if ($cas) {
1375
1376         # Is authentication against multiple CAS servers enabled?
1377         if ( C4::Auth_with_cas::multipleAuth && !$casparam ) {
1378             my $casservers = C4::Auth_with_cas::getMultipleAuth();
1379             my @tmplservers;
1380             foreach my $key ( keys %$casservers ) {
1381                 push @tmplservers, { name => $key, value => login_cas_url( $query, $key, $type ) . "?cas=$key" };
1382             }
1383             $template->param(
1384                 casServersLoop => \@tmplservers
1385             );
1386         } else {
1387             $template->param(
1388                 casServerUrl => login_cas_url($query, undef, $type),
1389             );
1390         }
1391
1392         $template->param(
1393             invalidCasLogin => $info{'invalidCasLogin'}
1394         );
1395     }
1396
1397     if ($shib) {
1398         $template->param(
1399             shibbolethAuthentication => $shib,
1400             shibbolethLoginUrl       => login_shib_url($query),
1401         );
1402     }
1403
1404     if (C4::Context->preference('GoogleOpenIDConnect')) {
1405         if ($query->param("OpenIDConnectFailed")) {
1406             my $reason = $query->param('OpenIDConnectFailed');
1407             $template->param(invalidGoogleOpenIDConnectLogin => $reason);
1408         }
1409     }
1410
1411     $template->param(
1412         LibraryName => C4::Context->preference("LibraryName"),
1413     );
1414     $template->param(%info);
1415
1416     #    $cookie = $query->cookie(CGISESSID => $session->id
1417     #   );
1418     print $query->header(
1419         {   type              => 'text/html',
1420             charset           => 'utf-8',
1421             cookie            => $cookie,
1422             'X-Frame-Options' => 'SAMEORIGIN'
1423         }
1424       ),
1425       $template->output;
1426     safe_exit;
1427 }
1428
1429 =head2 check_api_auth
1430
1431   ($status, $cookie, $sessionId) = check_api_auth($query, $userflags);
1432
1433 Given a CGI query containing the parameters 'userid' and 'password' and/or a session
1434 cookie, determine if the user has the privileges specified by C<$userflags>.
1435
1436 C<check_api_auth> is is meant for authenticating users of web services, and
1437 consequently will always return and will not attempt to redirect the user
1438 agent.
1439
1440 If a valid session cookie is already present, check_api_auth will return a status
1441 of "ok", the cookie, and the Koha session ID.
1442
1443 If no session cookie is present, check_api_auth will check the 'userid' and 'password
1444 parameters and create a session cookie and Koha session if the supplied credentials
1445 are OK.
1446
1447 Possible return values in C<$status> are:
1448
1449 =over
1450
1451 =item "ok" -- user authenticated; C<$cookie> and C<$sessionid> have valid values.
1452
1453 =item "failed" -- credentials are not correct; C<$cookie> and C<$sessionid> are undef
1454
1455 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1456
1457 =item "expired -- session cookie has expired; API user should resubmit userid and password
1458
1459 =back
1460
1461 =cut
1462
1463 sub check_api_auth {
1464
1465     my $query         = shift;
1466     my $flagsrequired = shift;
1467     my $dbh     = C4::Context->dbh;
1468     my $timeout = _timeout_syspref();
1469
1470     unless ( C4::Context->preference('Version') ) {
1471
1472         # database has not been installed yet
1473         return ( "maintenance", undef, undef );
1474     }
1475     my $kohaversion = Koha::version();
1476     $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1477     if ( C4::Context->preference('Version') < $kohaversion ) {
1478
1479         # database in need of version update; assume that
1480         # no API should be called while databsae is in
1481         # this condition.
1482         return ( "maintenance", undef, undef );
1483     }
1484
1485     # FIXME -- most of what follows is a copy-and-paste
1486     # of code from checkauth.  There is an obvious need
1487     # for refactoring to separate the various parts of
1488     # the authentication code, but as of 2007-11-19 this
1489     # is deferred so as to not introduce bugs into the
1490     # regular authentication code for Koha 3.0.
1491
1492     # see if we have a valid session cookie already
1493     # however, if a userid parameter is present (i.e., from
1494     # a form submission, assume that any current cookie
1495     # is to be ignored
1496     my $sessionID = undef;
1497     unless ( $query->param('userid') ) {
1498         $sessionID = $query->cookie("CGISESSID");
1499     }
1500     if ( $sessionID && not( $cas && $query->param('PT') ) ) {
1501         my $session = get_session($sessionID);
1502         C4::Context->_new_userenv($sessionID);
1503         if ($session) {
1504             C4::Context->interface($session->param('interface'));
1505             C4::Context->set_userenv(
1506                 $session->param('number'),       $session->param('id'),
1507                 $session->param('cardnumber'),   $session->param('firstname'),
1508                 $session->param('surname'),      $session->param('branch'),
1509                 $session->param('branchname'),   $session->param('flags'),
1510                 $session->param('emailaddress'), $session->param('shibboleth'),
1511                 $session->param('desk_id'),      $session->param('desk_name'),
1512                 $session->param('register_id'),  $session->param('register_name')
1513             );
1514
1515             my $ip       = $session->param('ip');
1516             my $lasttime = $session->param('lasttime');
1517             my $userid   = $session->param('id');
1518             if ( $lasttime < time() - $timeout ) {
1519
1520                 # time out
1521                 $session->delete();
1522                 $session->flush;
1523                 C4::Context->_unset_userenv($sessionID);
1524                 $userid    = undef;
1525                 $sessionID = undef;
1526                 return ( "expired", undef, undef );
1527             } elsif ( C4::Context->preference('SessionRestrictionByIP') && $ip ne $ENV{'REMOTE_ADDR'} ) {
1528
1529                 # IP address changed
1530                 $session->delete();
1531                 $session->flush;
1532                 C4::Context->_unset_userenv($sessionID);
1533                 $userid    = undef;
1534                 $sessionID = undef;
1535                 return ( "expired", undef, undef );
1536             } else {
1537                 my $cookie = $query->cookie(
1538                     -name     => 'CGISESSID',
1539                     -value    => $session->id,
1540                     -HttpOnly => 1,
1541                     -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1542                 );
1543                 $session->param( 'lasttime', time() );
1544                 my $flags = haspermission( $userid, $flagsrequired );
1545                 if ($flags) {
1546                     return ( "ok", $cookie, $sessionID );
1547                 } else {
1548                     $session->delete();
1549                     $session->flush;
1550                     C4::Context->_unset_userenv($sessionID);
1551                     $userid    = undef;
1552                     $sessionID = undef;
1553                     return ( "failed", undef, undef );
1554                 }
1555             }
1556         } else {
1557             return ( "expired", undef, undef );
1558         }
1559     } else {
1560
1561         # new login
1562         my $userid   = $query->param('userid');
1563         my $password = $query->param('password');
1564         my ( $return, $cardnumber, $cas_ticket );
1565
1566         # Proxy CAS auth
1567         if ( $cas && $query->param('PT') ) {
1568             my $retuserid;
1569             $debug and print STDERR "## check_api_auth - checking CAS\n";
1570
1571             # In case of a CAS authentication, we use the ticket instead of the password
1572             my $PT = $query->param('PT');
1573             ( $return, $cardnumber, $userid, $cas_ticket ) = check_api_auth_cas( $dbh, $PT, $query );    # EXTERNAL AUTH
1574         } else {
1575
1576             # User / password auth
1577             unless ( $userid and $password ) {
1578
1579                 # caller did something wrong, fail the authenticateion
1580                 return ( "failed", undef, undef );
1581             }
1582             my $newuserid;
1583             ( $return, $cardnumber, $newuserid, $cas_ticket ) = checkpw( $dbh, $userid, $password, $query );
1584         }
1585
1586         if ( $return and haspermission( $userid, $flagsrequired ) ) {
1587             my $session = get_session("");
1588             return ( "failed", undef, undef ) unless $session;
1589
1590             my $sessionID = $session->id;
1591             C4::Context->_new_userenv($sessionID);
1592             my $cookie = $query->cookie(
1593                 -name     => 'CGISESSID',
1594                 -value    => $sessionID,
1595                 -HttpOnly => 1,
1596                 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
1597             );
1598             if ( $return == 1 ) {
1599                 my (
1600                     $borrowernumber, $firstname,  $surname,
1601                     $userflags,      $branchcode, $branchname,
1602                     $emailaddress
1603                 );
1604                 my $sth =
1605                   $dbh->prepare(
1606 "select borrowernumber, firstname, surname, flags, borrowers.branchcode, branches.branchname as branchname, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where userid=?"
1607                   );
1608                 $sth->execute($userid);
1609                 (
1610                     $borrowernumber, $firstname,  $surname,
1611                     $userflags,      $branchcode, $branchname,
1612                     $emailaddress
1613                 ) = $sth->fetchrow if ( $sth->rows );
1614
1615                 unless ( $sth->rows ) {
1616                     my $sth = $dbh->prepare(
1617 "select borrowernumber, firstname, surname, flags, borrowers.branchcode, branches.branchname as branchname, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where cardnumber=?"
1618                     );
1619                     $sth->execute($cardnumber);
1620                     (
1621                         $borrowernumber, $firstname,  $surname,
1622                         $userflags,      $branchcode, $branchname,
1623                         $emailaddress
1624                     ) = $sth->fetchrow if ( $sth->rows );
1625
1626                     unless ( $sth->rows ) {
1627                         $sth->execute($userid);
1628                         (
1629                             $borrowernumber, $firstname,  $surname,       $userflags,
1630                             $branchcode,     $branchname, $emailaddress
1631                         ) = $sth->fetchrow if ( $sth->rows );
1632                     }
1633                 }
1634
1635                 my $ip = $ENV{'REMOTE_ADDR'};
1636
1637                 # if they specify at login, use that
1638                 if ( $query->param('branch') ) {
1639                     $branchcode = $query->param('branch');
1640                     my $library = Koha::Libraries->find($branchcode);
1641                     $branchname = $library? $library->branchname: '';
1642                 }
1643                 my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search };
1644                 foreach my $br ( keys %$branches ) {
1645
1646                     #     now we work with the treatment of ip
1647                     my $domain = $branches->{$br}->{'branchip'};
1648                     if ( $domain && $ip =~ /^$domain/ ) {
1649                         $branchcode = $branches->{$br}->{'branchcode'};
1650
1651                         # new op dev : add the branchname to the cookie
1652                         $branchname    = $branches->{$br}->{'branchname'};
1653                     }
1654                 }
1655                 $session->param( 'number',       $borrowernumber );
1656                 $session->param( 'id',           $userid );
1657                 $session->param( 'cardnumber',   $cardnumber );
1658                 $session->param( 'firstname',    $firstname );
1659                 $session->param( 'surname',      $surname );
1660                 $session->param( 'branch',       $branchcode );
1661                 $session->param( 'branchname',   $branchname );
1662                 $session->param( 'flags',        $userflags );
1663                 $session->param( 'emailaddress', $emailaddress );
1664                 $session->param( 'ip',           $session->remote_addr() );
1665                 $session->param( 'lasttime',     time() );
1666                 $session->param( 'interface',    'api'  );
1667             }
1668             $session->param( 'cas_ticket', $cas_ticket);
1669             C4::Context->set_userenv(
1670                 $session->param('number'),       $session->param('id'),
1671                 $session->param('cardnumber'),   $session->param('firstname'),
1672                 $session->param('surname'),      $session->param('branch'),
1673                 $session->param('branchname'),   $session->param('flags'),
1674                 $session->param('emailaddress'), $session->param('shibboleth'),
1675                 $session->param('desk_id'),      $session->param('desk_name'),
1676                 $session->param('register_id'),  $session->param('register_name')
1677             );
1678             return ( "ok", $cookie, $sessionID );
1679         } else {
1680             return ( "failed", undef, undef );
1681         }
1682     }
1683 }
1684
1685 =head2 check_cookie_auth
1686
1687   ($status, $sessionId) = check_api_auth($cookie, $userflags);
1688
1689 Given a CGISESSID cookie set during a previous login to Koha, determine
1690 if the user has the privileges specified by C<$userflags>. C<$userflags>
1691 is passed unaltered into C<haspermission> and as such accepts all options
1692 avaiable to that routine with the one caveat that C<check_api_auth> will
1693 also allow 'undef' to be passed and in such a case the permissions check
1694 will be skipped altogether.
1695
1696 C<check_cookie_auth> is meant for authenticating special services
1697 such as tools/upload-file.pl that are invoked by other pages that
1698 have been authenticated in the usual way.
1699
1700 Possible return values in C<$status> are:
1701
1702 =over
1703
1704 =item "ok" -- user authenticated; C<$sessionID> have valid values.
1705
1706 =item "failed" -- credentials are not correct; C<$sessionid> are undef
1707
1708 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1709
1710 =item "expired -- session cookie has expired; API user should resubmit userid and password
1711
1712 =back
1713
1714 =cut
1715
1716 sub check_cookie_auth {
1717     my $cookie        = shift;
1718     my $flagsrequired = shift;
1719     my $params        = shift;
1720
1721     my $remote_addr = $params->{remote_addr} || $ENV{REMOTE_ADDR};
1722     my $dbh     = C4::Context->dbh;
1723     my $timeout = _timeout_syspref();
1724
1725     unless ( C4::Context->preference('Version') ) {
1726
1727         # database has not been installed yet
1728         return ( "maintenance", undef );
1729     }
1730     my $kohaversion = Koha::version();
1731     $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1732     if ( C4::Context->preference('Version') < $kohaversion ) {
1733
1734         # database in need of version update; assume that
1735         # no API should be called while databsae is in
1736         # this condition.
1737         return ( "maintenance", undef );
1738     }
1739
1740     # FIXME -- most of what follows is a copy-and-paste
1741     # of code from checkauth.  There is an obvious need
1742     # for refactoring to separate the various parts of
1743     # the authentication code, but as of 2007-11-23 this
1744     # is deferred so as to not introduce bugs into the
1745     # regular authentication code for Koha 3.0.
1746
1747     # see if we have a valid session cookie already
1748     # however, if a userid parameter is present (i.e., from
1749     # a form submission, assume that any current cookie
1750     # is to be ignored
1751     unless ( defined $cookie and $cookie ) {
1752         return ( "failed", undef );
1753     }
1754     my $sessionID = $cookie;
1755     my $session   = get_session($sessionID);
1756     C4::Context->_new_userenv($sessionID);
1757     if ($session) {
1758         C4::Context->interface($session->param('interface'));
1759         C4::Context->set_userenv(
1760             $session->param('number'),       $session->param('id'),
1761             $session->param('cardnumber'),   $session->param('firstname'),
1762             $session->param('surname'),      $session->param('branch'),
1763             $session->param('branchname'),   $session->param('flags'),
1764             $session->param('emailaddress'), $session->param('shibboleth'),
1765             $session->param('desk_id'),      $session->param('desk_name'),
1766             $session->param('register_id'),  $session->param('register_name')
1767         );
1768
1769         my $ip       = $session->param('ip');
1770         my $lasttime = $session->param('lasttime');
1771         my $userid   = $session->param('id');
1772         if ( $lasttime < time() - $timeout ) {
1773
1774             # time out
1775             $session->delete();
1776             $session->flush;
1777             C4::Context->_unset_userenv($sessionID);
1778             $userid    = undef;
1779             $sessionID = undef;
1780             return ("expired", undef);
1781         } elsif ( C4::Context->preference('SessionRestrictionByIP') && $ip ne $remote_addr ) {
1782
1783             # IP address changed
1784             $session->delete();
1785             $session->flush;
1786             C4::Context->_unset_userenv($sessionID);
1787             $userid    = undef;
1788             $sessionID = undef;
1789             return ( "expired", undef );
1790         } else {
1791             $session->param( 'lasttime', time() );
1792             my $flags = defined($flagsrequired) ? haspermission( $userid, $flagsrequired ) : 1;
1793             if ($flags) {
1794                 return ( "ok", $sessionID );
1795             } else {
1796                 $session->delete();
1797                 $session->flush;
1798                 C4::Context->_unset_userenv($sessionID);
1799                 $userid    = undef;
1800                 $sessionID = undef;
1801                 return ( "failed", undef );
1802             }
1803         }
1804     } else {
1805         return ( "expired", undef );
1806     }
1807 }
1808
1809 =head2 get_session
1810
1811   use CGI::Session;
1812   my $session = get_session($sessionID);
1813
1814 Given a session ID, retrieve the CGI::Session object used to store
1815 the session's state.  The session object can be used to store
1816 data that needs to be accessed by different scripts during a
1817 user's session.
1818
1819 If the C<$sessionID> parameter is an empty string, a new session
1820 will be created.
1821
1822 =cut
1823
1824 sub _get_session_params {
1825     my $storage_method = C4::Context->preference('SessionStorage');
1826     if ( $storage_method eq 'mysql' ) {
1827         my $dbh = C4::Context->dbh;
1828         return { dsn => "driver:MySQL;serializer:yaml;id:md5", dsn_args => { Handle => $dbh } };
1829     }
1830     elsif ( $storage_method eq 'Pg' ) {
1831         my $dbh = C4::Context->dbh;
1832         return { dsn => "driver:PostgreSQL;serializer:yaml;id:md5", dsn_args => { Handle => $dbh } };
1833     }
1834     elsif ( $storage_method eq 'memcached' && Koha::Caches->get_instance->memcached_cache ) {
1835         my $memcached = Koha::Caches->get_instance()->memcached_cache;
1836         return { dsn => "driver:memcached;serializer:yaml;id:md5", dsn_args => { Memcached => $memcached } };
1837     }
1838     else {
1839         # catch all defaults to tmp should work on all systems
1840         my $dir = C4::Context::temporary_directory;
1841         my $instance = C4::Context->config( 'database' ); #actually for packages not exactly the instance name, but generally safer to leave it as it is
1842         return { dsn => "driver:File;serializer:yaml;id:md5", dsn_args => { Directory => "$dir/cgisess_$instance" } };
1843     }
1844 }
1845
1846 sub get_session {
1847     my $sessionID      = shift;
1848     my $params = _get_session_params();
1849     return CGI::Session->new( $params->{dsn}, $sessionID, $params->{dsn_args} );
1850 }
1851
1852
1853 # FIXME no_set_userenv may be replaced with force_branchcode_for_userenv
1854 # (or something similar)
1855 # Currently it's only passed from C4::SIP::ILS::Patron::check_password, but
1856 # not having a userenv defined could cause a crash.
1857 sub checkpw {
1858     my ( $dbh, $userid, $password, $query, $type, $no_set_userenv ) = @_;
1859     $type = 'opac' unless $type;
1860
1861     # Get shibboleth login attribute
1862     my $shib = C4::Context->config('useshibboleth') && shib_ok();
1863     my $shib_login = $shib ? get_login_shib() : undef;
1864
1865     my @return;
1866     my $patron;
1867     if ( defined $userid ){
1868         $patron = Koha::Patrons->find({ userid => $userid });
1869         $patron = Koha::Patrons->find({ cardnumber => $userid }) unless $patron;
1870     }
1871     my $check_internal_as_fallback = 0;
1872     my $passwd_ok = 0;
1873     # Note: checkpw_* routines returns:
1874     # 1 if auth is ok
1875     # 0 if auth is nok
1876     # -1 if user bind failed (LDAP only)
1877
1878     if ( $patron and $patron->account_locked ) {
1879         # Nothing to check, account is locked
1880     } elsif ($ldap && defined($password)) {
1881         $debug and print STDERR "## checkpw - checking LDAP\n";
1882         my ( $retval, $retcard, $retuserid ) = checkpw_ldap(@_);    # EXTERNAL AUTH
1883         if ( $retval == 1 ) {
1884             @return = ( $retval, $retcard, $retuserid );
1885             $passwd_ok = 1;
1886         }
1887         $check_internal_as_fallback = 1 if $retval == 0;
1888
1889     } elsif ( $cas && $query && $query->param('ticket') ) {
1890         $debug and print STDERR "## checkpw - checking CAS\n";
1891
1892         # In case of a CAS authentication, we use the ticket instead of the password
1893         my $ticket = $query->param('ticket');
1894         $query->delete('ticket');                                   # remove ticket to come back to original URL
1895         my ( $retval, $retcard, $retuserid, $cas_ticket ) = checkpw_cas( $dbh, $ticket, $query, $type );    # EXTERNAL AUTH
1896         if ( $retval ) {
1897             @return = ( $retval, $retcard, $retuserid, $cas_ticket );
1898         } else {
1899             @return = (0);
1900         }
1901         $passwd_ok = $retval;
1902     }
1903
1904     # If we are in a shibboleth session (shibboleth is enabled, and a shibboleth match attribute is present)
1905     # Check for password to asertain whether we want to be testing against shibboleth or another method this
1906     # time around.
1907     elsif ( $shib && $shib_login && !$password ) {
1908
1909         $debug and print STDERR "## checkpw - checking Shibboleth\n";
1910
1911         # In case of a Shibboleth authentication, we expect a shibboleth user attribute
1912         # (defined under shibboleth mapping in koha-conf.xml) to contain the login of the
1913         # shibboleth-authenticated user
1914
1915         # Then, we check if it matches a valid koha user
1916         if ($shib_login) {
1917             my ( $retval, $retcard, $retuserid ) = C4::Auth_with_shibboleth::checkpw_shib($shib_login);    # EXTERNAL AUTH
1918             if ( $retval ) {
1919                 @return = ( $retval, $retcard, $retuserid );
1920             }
1921             $passwd_ok = $retval;
1922         }
1923     } else {
1924         $check_internal_as_fallback = 1;
1925     }
1926
1927     # INTERNAL AUTH
1928     if ( $check_internal_as_fallback ) {
1929         @return = checkpw_internal( $dbh, $userid, $password, $no_set_userenv);
1930         $passwd_ok = 1 if $return[0] > 0; # 1 or 2
1931     }
1932
1933     if( $patron ) {
1934         if ( $passwd_ok ) {
1935             $patron->update({ login_attempts => 0 });
1936         } elsif( !$patron->account_locked ) {
1937             $patron->update({ login_attempts => $patron->login_attempts + 1 });
1938         }
1939     }
1940
1941     # Optionally log success or failure
1942     if( $patron && $passwd_ok && C4::Context->preference('AuthSuccessLog') ) {
1943         logaction( 'AUTH', 'SUCCESS', $patron->id, "Valid password for $userid", $type );
1944     } elsif( !$passwd_ok && C4::Context->preference('AuthFailureLog') ) {
1945         logaction( 'AUTH', 'FAILURE', $patron ? $patron->id : 0, "Wrong password for $userid", $type );
1946     }
1947
1948     return @return;
1949 }
1950
1951 sub checkpw_internal {
1952     my ( $dbh, $userid, $password, $no_set_userenv ) = @_;
1953
1954     $password = Encode::encode( 'UTF-8', $password )
1955       if Encode::is_utf8($password);
1956
1957     my $sth =
1958       $dbh->prepare(
1959         "select password,cardnumber,borrowernumber,userid,firstname,surname,borrowers.branchcode,branches.branchname,flags from borrowers join branches on borrowers.branchcode=branches.branchcode where userid=?"
1960       );
1961     $sth->execute($userid);
1962     if ( $sth->rows ) {
1963         my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
1964             $surname, $branchcode, $branchname, $flags )
1965           = $sth->fetchrow;
1966
1967         if ( checkpw_hash( $password, $stored_hash ) ) {
1968
1969             C4::Context->set_userenv( "$borrowernumber", $userid, $cardnumber,
1970                 $firstname, $surname, $branchcode, $branchname, $flags ) unless $no_set_userenv;
1971             return 1, $cardnumber, $userid;
1972         }
1973     }
1974     $sth =
1975       $dbh->prepare(
1976         "select password,cardnumber,borrowernumber,userid,firstname,surname,borrowers.branchcode,branches.branchname,flags from borrowers join branches on borrowers.branchcode=branches.branchcode where cardnumber=?"
1977       );
1978     $sth->execute($userid);
1979     if ( $sth->rows ) {
1980         my ( $stored_hash, $cardnumber, $borrowernumber, $userid, $firstname,
1981             $surname, $branchcode, $branchname, $flags )
1982           = $sth->fetchrow;
1983
1984         if ( checkpw_hash( $password, $stored_hash ) ) {
1985
1986             C4::Context->set_userenv( $borrowernumber, $userid, $cardnumber,
1987                 $firstname, $surname, $branchcode, $branchname, $flags ) unless $no_set_userenv;
1988             return 1, $cardnumber, $userid;
1989         }
1990     }
1991     return 0;
1992 }
1993
1994 sub checkpw_hash {
1995     my ( $password, $stored_hash ) = @_;
1996
1997     return if $stored_hash eq '!';
1998
1999     # check what encryption algorithm was implemented: Bcrypt - if the hash starts with '$2' it is Bcrypt else md5
2000     my $hash;
2001     if ( substr( $stored_hash, 0, 2 ) eq '$2' ) {
2002         $hash = hash_password( $password, $stored_hash );
2003     } else {
2004         $hash = md5_base64($password);
2005     }
2006     return $hash eq $stored_hash;
2007 }
2008
2009 =head2 getuserflags
2010
2011     my $authflags = getuserflags($flags, $userid, [$dbh]);
2012
2013 Translates integer flags into permissions strings hash.
2014
2015 C<$flags> is the integer userflags value ( borrowers.userflags )
2016 C<$userid> is the members.userid, used for building subpermissions
2017 C<$authflags> is a hashref of permissions
2018
2019 =cut
2020
2021 sub getuserflags {
2022     my $flags  = shift;
2023     my $userid = shift;
2024     my $dbh    = @_ ? shift : C4::Context->dbh;
2025     my $userflags;
2026     {
2027         # I don't want to do this, but if someone logs in as the database
2028         # user, it would be preferable not to spam them to death with
2029         # numeric warnings. So, we make $flags numeric.
2030         no warnings 'numeric';
2031         $flags += 0;
2032     }
2033     my $sth = $dbh->prepare("SELECT bit, flag, defaulton FROM userflags");
2034     $sth->execute;
2035
2036     while ( my ( $bit, $flag, $defaulton ) = $sth->fetchrow ) {
2037         if ( ( $flags & ( 2**$bit ) ) || $defaulton ) {
2038             $userflags->{$flag} = 1;
2039         }
2040         else {
2041             $userflags->{$flag} = 0;
2042         }
2043     }
2044
2045     # get subpermissions and merge with top-level permissions
2046     my $user_subperms = get_user_subpermissions($userid);
2047     foreach my $module ( keys %$user_subperms ) {
2048         next if $userflags->{$module} == 1;    # user already has permission for everything in this module
2049         $userflags->{$module} = $user_subperms->{$module};
2050     }
2051
2052     return $userflags;
2053 }
2054
2055 =head2 get_user_subpermissions
2056
2057   $user_perm_hashref = get_user_subpermissions($userid);
2058
2059 Given the userid (note, not the borrowernumber) of a staff user,
2060 return a hashref of hashrefs of the specific subpermissions
2061 accorded to the user.  An example return is
2062
2063  {
2064     tools => {
2065         export_catalog => 1,
2066         import_patrons => 1,
2067     }
2068  }
2069
2070 The top-level hash-key is a module or function code from
2071 userflags.flag, while the second-level key is a code
2072 from permissions.
2073
2074 The results of this function do not give a complete picture
2075 of the functions that a staff user can access; it is also
2076 necessary to check borrowers.flags.
2077
2078 =cut
2079
2080 sub get_user_subpermissions {
2081     my $userid = shift;
2082
2083     my $dbh = C4::Context->dbh;
2084     my $sth = $dbh->prepare( "SELECT flag, user_permissions.code
2085                              FROM user_permissions
2086                              JOIN permissions USING (module_bit, code)
2087                              JOIN userflags ON (module_bit = bit)
2088                              JOIN borrowers USING (borrowernumber)
2089                              WHERE userid = ?" );
2090     $sth->execute($userid);
2091
2092     my $user_perms = {};
2093     while ( my $perm = $sth->fetchrow_hashref ) {
2094         $user_perms->{ $perm->{'flag'} }->{ $perm->{'code'} } = 1;
2095     }
2096     return $user_perms;
2097 }
2098
2099 =head2 get_all_subpermissions
2100
2101   my $perm_hashref = get_all_subpermissions();
2102
2103 Returns a hashref of hashrefs defining all specific
2104 permissions currently defined.  The return value
2105 has the same structure as that of C<get_user_subpermissions>,
2106 except that the innermost hash value is the description
2107 of the subpermission.
2108
2109 =cut
2110
2111 sub get_all_subpermissions {
2112     my $dbh = C4::Context->dbh;
2113     my $sth = $dbh->prepare( "SELECT flag, code
2114                              FROM permissions
2115                              JOIN userflags ON (module_bit = bit)" );
2116     $sth->execute();
2117
2118     my $all_perms = {};
2119     while ( my $perm = $sth->fetchrow_hashref ) {
2120         $all_perms->{ $perm->{'flag'} }->{ $perm->{'code'} } = 1;
2121     }
2122     return $all_perms;
2123 }
2124
2125 =head2 haspermission
2126
2127   $flagsrequired = '*';                                 # Any permission at all
2128   $flagsrequired = 'a_flag';                            # a_flag must be satisfied (all subpermissions)
2129   $flagsrequired = [ 'a_flag', 'b_flag' ];              # a_flag OR b_flag must be satisfied
2130   $flagsrequired = { 'a_flag => 1, 'b_flag' => 1 };     # a_flag AND b_flag must be satisfied
2131   $flagsrequired = { 'a_flag' => 'sub_a' };             # sub_a of a_flag must be satisfied
2132   $flagsrequired = { 'a_flag' => [ 'sub_a, 'sub_b' ] }; # sub_a OR sub_b of a_flag must be satisfied
2133
2134   $flags = ($userid, $flagsrequired);
2135
2136 C<$userid> the userid of the member
2137 C<$flags> is a query structure similar to that used by SQL::Abstract that
2138 denotes the combination of flags required. It is a required parameter.
2139
2140 The main logic of this method is that things in arrays are OR'ed, and things
2141 in hashes are AND'ed. The `*` character can be used, at any depth, to denote `ANY`
2142
2143 Returns member's flags or 0 if a permission is not met.
2144
2145 =cut
2146
2147 sub _dispatch {
2148     my ($required, $flags) = @_;
2149
2150     my $ref = ref($required);
2151     if ($ref eq '') {
2152         if ($required eq '*') {
2153             return 0 unless ( $flags or ref( $flags ) );
2154         } else {
2155             return 0 unless ( $flags and (!ref( $flags ) || $flags->{$required} ));
2156         }
2157     } elsif ($ref eq 'HASH') {
2158         foreach my $key (keys %{$required}) {
2159             next if $flags == 1;
2160             my $require = $required->{$key};
2161             my $rflags  = $flags->{$key};
2162             return 0 unless _dispatch($require, $rflags);
2163         }
2164     } elsif ($ref eq 'ARRAY') {
2165         my $satisfied = 0;
2166         foreach my $require ( @{$required} ) {
2167             my $rflags =
2168               ( ref($flags) && !ref($require) && ( $require ne '*' ) )
2169               ? $flags->{$require}
2170               : $flags;
2171             $satisfied++ if _dispatch( $require, $rflags );
2172         }
2173         return 0 unless $satisfied;
2174     } else {
2175         croak "Unexpected structure found: $ref";
2176     }
2177
2178     return $flags;
2179 };
2180
2181 sub haspermission {
2182     my ( $userid, $flagsrequired ) = @_;
2183
2184     #Koha::Exceptions::WrongParameter->throw('$flagsrequired should not be undef')
2185     #  unless defined($flagsrequired);
2186
2187     my $sth = C4::Context->dbh->prepare("SELECT flags FROM borrowers WHERE userid=?");
2188     $sth->execute($userid);
2189     my $row = $sth->fetchrow();
2190     my $flags = getuserflags( $row, $userid );
2191
2192     return $flags unless defined($flagsrequired);
2193     return $flags if $flags->{superlibrarian};
2194     return _dispatch($flagsrequired, $flags);
2195
2196     #FIXME - This fcn should return the failed permission so a suitable error msg can be delivered.
2197 }
2198
2199 =head2 in_iprange
2200
2201   $flags = ($iprange);
2202
2203 C<$iprange> A space separated string describing an IP range. Can include single IPs or ranges
2204
2205 Returns 1 if the remote address is in the provided iprange, or 0 otherwise.
2206
2207 =cut
2208
2209 sub in_iprange {
2210     my ($iprange) = @_;
2211     my $result = 1;
2212     my @allowedipranges = $iprange ? split(' ', $iprange) : ();
2213     if (scalar @allowedipranges > 0) {
2214         my @rangelist;
2215         eval { @rangelist = Net::CIDR::range2cidr(@allowedipranges); }; return 0 if $@;
2216         eval { $result = Net::CIDR::cidrlookup($ENV{'REMOTE_ADDR'}, @rangelist) } || ( $ENV{DEBUG} && warn 'cidrlookup failed for ' . join(' ',@rangelist) );
2217      }
2218      return $result ? 1 : 0;
2219 }
2220
2221 sub getborrowernumber {
2222     my ($userid) = @_;
2223     my $userenv = C4::Context->userenv;
2224     if ( defined($userenv) && ref($userenv) eq 'HASH' && $userenv->{number} ) {
2225         return $userenv->{number};
2226     }
2227     my $dbh = C4::Context->dbh;
2228     for my $field ( 'userid', 'cardnumber' ) {
2229         my $sth =
2230           $dbh->prepare("select borrowernumber from borrowers where $field=?");
2231         $sth->execute($userid);
2232         if ( $sth->rows ) {
2233             my ($bnumber) = $sth->fetchrow;
2234             return $bnumber;
2235         }
2236     }
2237     return 0;
2238 }
2239
2240 =head2 track_login_daily
2241
2242     track_login_daily( $userid );
2243
2244 Wraps the call to $patron->track_login, the method used to update borrowers.lastseen. We only call track_login once a day.
2245
2246 =cut
2247
2248 sub track_login_daily {
2249     my $userid = shift;
2250     return if !$userid || !C4::Context->preference('TrackLastPatronActivity');
2251
2252     my $cache     = Koha::Caches->get_instance();
2253     my $cache_key = "track_login_" . $userid;
2254     my $cached    = $cache->get_from_cache($cache_key);
2255     my $today = dt_from_string()->ymd;
2256     return if $cached && $cached eq $today;
2257
2258     my $patron = Koha::Patrons->find({ userid => $userid });
2259     return unless $patron;
2260     $patron->track_login;
2261     $cache->set_in_cache( $cache_key, $today );
2262 }
2263
2264 END { }    # module clean-up code here (global destructor)
2265 1;
2266 __END__
2267
2268 =head1 SEE ALSO
2269
2270 CGI(3)
2271
2272 C4::Output(3)
2273
2274 Crypt::Eksblowfish::Bcrypt(3)
2275
2276 Digest::MD5(3)
2277
2278 =cut