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