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