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