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