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