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