Bug 15492: Add SCI module
[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
183     # If the user logged in is the SCO user and they try to go out of the SCO module, log the user out removing the CGISESSID cookie
184     if ( $in->{type} eq 'opac' and $in->{template_name} !~ m|sco/| ) {
185         if ( $user && C4::Context->preference('AutoSelfCheckID') && $user eq C4::Context->preference('AutoSelfCheckID') ) {
186             $template = C4::Templates::gettemplate( 'opac-auth.tt', 'opac', $in->{query} );
187             my $cookie = $in->{query}->cookie(
188                 -name     => 'CGISESSID',
189                 -value    => '',
190                 -expires  => '',
191                 -HttpOnly => 1,
192             );
193
194             $template->param(
195                 loginprompt => 1,
196                 script_name => get_script_name(),
197             );
198             print $in->{query}->header(
199                 {   type              => 'text/html',
200                     charset           => 'utf-8',
201                     cookie            => $cookie,
202                     'X-Frame-Options' => 'SAMEORIGIN'
203                 }
204               ),
205             $template->output;
206             safe_exit;
207         }
208     }
209
210     # If the user logged in is the SCI user and they try to go out of the SCI module,
211     # log the user out removing the CGISESSID cookie
212     if ( $in->{type} eq 'opac' and $in->{template_name} !~ m|sci/| ) {
213         if ( $user && C4::Context->preference('AutoSelfCheckID') && $user eq C4::Context->preference('AutoSelfCheckID') ) {
214             $template = C4::Templates::gettemplate( 'opac-auth.tt', 'opac', $in->{query} );
215             my $cookie = $in->{query}->cookie(
216                 -name     => 'CGISESSID',
217                 -value    => '',
218                 -expires  => '',
219                 -HttpOnly => 1,
220             );
221
222             $template->param(
223                 loginprompt => 1,
224                 script_name => get_script_name(),
225             );
226             print $in->{query}->header(
227                 {   type              => 'text/html',
228                     charset           => 'utf-8',
229                     cookie            => $cookie,
230                     'X-Frame-Options' => 'SAMEORIGIN'
231                 }
232               ),
233             $template->output;
234             safe_exit;
235         }
236     }
237
238     my $borrowernumber;
239     if ($user) {
240
241         # It's possible for $user to be the borrowernumber if they don't have a
242         # userid defined (and are logging in through some other method, such
243         # as SSL certs against an email address)
244         my $patron;
245         $borrowernumber = getborrowernumber($user) if defined($user);
246         if ( !defined($borrowernumber) && defined($user) ) {
247             $patron = Koha::Patrons->find( $user );
248             if ($patron) {
249                 $borrowernumber = $user;
250
251                 # A bit of a hack, but I don't know there's a nicer way
252                 # to do it.
253                 $user = $patron->firstname . ' ' . $patron->surname;
254             }
255         } else {
256             $patron = Koha::Patrons->find( $borrowernumber );
257             # FIXME What to do if $patron does not exist?
258         }
259
260         # user info
261         $template->param( loggedinusername   => $user ); # FIXME Should be replaced with something like patron-title.inc
262         $template->param( loggedinusernumber => $borrowernumber ); # FIXME Should be replaced with logged_in_user.borrowernumber
263         $template->param( logged_in_user     => $patron );
264         $template->param( sessionID          => $sessionID );
265
266         if ( $in->{'type'} eq 'opac' ) {
267             require Koha::Virtualshelves;
268             my $some_private_shelves = Koha::Virtualshelves->get_some_shelves(
269                 {
270                     borrowernumber => $borrowernumber,
271                     category       => 1,
272                 }
273             );
274             my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
275                 {
276                     category       => 2,
277                 }
278             );
279             $template->param(
280                 some_private_shelves => $some_private_shelves,
281                 some_public_shelves  => $some_public_shelves,
282             );
283         }
284
285         $template->param( "USER_INFO" => $patron->unblessed ) if $borrowernumber != 0;
286
287         my $all_perms = get_all_subpermissions();
288
289         my @flagroots = qw(circulate catalogue parameters borrowers permissions reserveforothers borrow
290           editcatalogue updatecharges management tools editauthorities serials reports acquisition clubs);
291
292         # We are going to use the $flags returned by checkauth
293         # to create the template's parameters that will indicate
294         # which menus the user can access.
295         if ( $flags && $flags->{superlibrarian} == 1 ) {
296             $template->param( CAN_user_circulate        => 1 );
297             $template->param( CAN_user_catalogue        => 1 );
298             $template->param( CAN_user_parameters       => 1 );
299             $template->param( CAN_user_borrowers        => 1 );
300             $template->param( CAN_user_permissions      => 1 );
301             $template->param( CAN_user_reserveforothers => 1 );
302             $template->param( CAN_user_editcatalogue    => 1 );
303             $template->param( CAN_user_updatecharges    => 1 );
304             $template->param( CAN_user_acquisition      => 1 );
305             $template->param( CAN_user_management       => 1 );
306             $template->param( CAN_user_tools            => 1 );
307             $template->param( CAN_user_editauthorities  => 1 );
308             $template->param( CAN_user_serials          => 1 );
309             $template->param( CAN_user_reports          => 1 );
310             $template->param( CAN_user_staffaccess      => 1 );
311             $template->param( CAN_user_plugins          => 1 );
312             $template->param( CAN_user_coursereserves   => 1 );
313             $template->param( CAN_user_clubs            => 1 );
314
315             foreach my $module ( keys %$all_perms ) {
316                 foreach my $subperm ( keys %{ $all_perms->{$module} } ) {
317                     $template->param( "CAN_user_${module}_${subperm}" => 1 );
318                 }
319             }
320         }
321
322         if ($flags) {
323             foreach my $module ( keys %$all_perms ) {
324                 if ( defined($flags->{$module}) && $flags->{$module} == 1 ) {
325                     foreach my $subperm ( keys %{ $all_perms->{$module} } ) {
326                         $template->param( "CAN_user_${module}_${subperm}" => 1 );
327                     }
328                 } elsif ( ref( $flags->{$module} ) ) {
329                     foreach my $subperm ( keys %{ $flags->{$module} } ) {
330                         $template->param( "CAN_user_${module}_${subperm}" => 1 );
331                     }
332                 }
333             }
334         }
335
336         if ($flags) {
337             foreach my $module ( keys %$flags ) {
338                 if ( $flags->{$module} == 1 or ref( $flags->{$module} ) ) {
339                     $template->param( "CAN_user_$module" => 1 );
340                     if ( $module eq "parameters" ) {
341                         $template->param( CAN_user_management => 1 );
342                     }
343                 }
344             }
345         }
346
347         # Logged-in opac search history
348         # If the requested template is an opac one and opac search history is enabled
349         if ( $in->{type} eq 'opac' && C4::Context->preference('EnableOpacSearchHistory') ) {
350             my $dbh   = C4::Context->dbh;
351             my $query = "SELECT COUNT(*) FROM search_history WHERE userid=?";
352             my $sth   = $dbh->prepare($query);
353             $sth->execute($borrowernumber);
354
355             # If at least one search has already been performed
356             if ( $sth->fetchrow_array > 0 ) {
357
358                 # We show the link in opac
359                 $template->param( EnableOpacSearchHistory => 1 );
360             }
361             if (C4::Context->preference('LoadSearchHistoryToTheFirstLoggedUser'))
362             {
363                 # And if there are searches performed when the user was not logged in,
364                 # we add them to the logged-in search history
365                 my @recentSearches = C4::Search::History::get_from_session( { cgi => $in->{'query'} } );
366                 if (@recentSearches) {
367                     my $dbh   = C4::Context->dbh;
368                     my $query = q{
369                         INSERT INTO search_history(userid, sessionid, query_desc, query_cgi, type,  total, time )
370                         VALUES (?, ?, ?, ?, ?, ?, ?)
371                     };
372                     my $sth = $dbh->prepare($query);
373                     $sth->execute( $borrowernumber,
374                         $in->{query}->cookie("CGISESSID"),
375                         $_->{query_desc},
376                         $_->{query_cgi},
377                         $_->{type} || 'biblio',
378                         $_->{total},
379                         $_->{time},
380                     ) foreach @recentSearches;
381
382                     # clear out the search history from the session now that
383                     # we've saved it to the database
384                  }
385               }
386               C4::Search::History::set_to_session( { cgi => $in->{'query'}, search_history => [] } );
387
388         } elsif ( $in->{type} eq 'intranet' and C4::Context->preference('EnableSearchHistory') ) {
389             $template->param( EnableSearchHistory => 1 );
390         }
391     }
392     else {    # if this is an anonymous session, setup to display public lists...
393
394         # If shibboleth is enabled, and we're in an anonymous session, we should allow
395         # the user to attempt login via shibboleth.
396         if ($shib) {
397             $template->param( shibbolethAuthentication => $shib,
398                 shibbolethLoginUrl => login_shib_url( $in->{'query'} ),
399             );
400
401             # If shibboleth is enabled and we have a shibboleth login attribute,
402             # but we are in an anonymous session, then we clearly have an invalid
403             # shibboleth koha account.
404             if ($shib_login) {
405                 $template->param( invalidShibLogin => '1' );
406             }
407         }
408
409         $template->param( sessionID => $sessionID );
410
411         if ( $in->{'type'} eq 'opac' ){
412             require Koha::Virtualshelves;
413             my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
414                 {
415                     category       => 2,
416                 }
417             );
418             $template->param(
419                 some_public_shelves  => $some_public_shelves,
420             );
421         }
422     }
423
424     # Anonymous opac search history
425     # If opac search history is enabled and at least one search has already been performed
426     if ( C4::Context->preference('EnableOpacSearchHistory') ) {
427         my @recentSearches = C4::Search::History::get_from_session( { cgi => $in->{'query'} } );
428         if (@recentSearches) {
429             $template->param( EnableOpacSearchHistory => 1 );
430         }
431     }
432
433     if ( C4::Context->preference('dateformat') ) {
434         $template->param( dateformat => C4::Context->preference('dateformat') );
435     }
436
437     $template->param(auth_forwarded_hash => scalar $in->{'query'}->param('auth_forwarded_hash'));
438
439     # these template parameters are set the same regardless of $in->{'type'}
440
441     # Set the using_https variable for templates
442     # FIXME Under Plack the CGI->https method always returns 'OFF'
443     my $https = $in->{query}->https();
444     my $using_https = ( defined $https and $https ne 'OFF' ) ? 1 : 0;
445
446     my $minPasswordLength = C4::Context->preference('minPasswordLength');
447     $minPasswordLength = 3 if not $minPasswordLength or $minPasswordLength < 3;
448     $template->param(
449         "BiblioDefaultView" . C4::Context->preference("BiblioDefaultView") => 1,
450         EnhancedMessagingPreferences                                       => C4::Context->preference('EnhancedMessagingPreferences'),
451         GoogleJackets                                                      => C4::Context->preference("GoogleJackets"),
452         OpenLibraryCovers                                                  => C4::Context->preference("OpenLibraryCovers"),
453         KohaAdminEmailAddress                                              => "" . C4::Context->preference("KohaAdminEmailAddress"),
454         LoginBranchcode => ( C4::Context->userenv ? C4::Context->userenv->{"branch"}    : undef ),
455         LoginFirstname  => ( C4::Context->userenv ? C4::Context->userenv->{"firstname"} : "Bel" ),
456         LoginSurname    => C4::Context->userenv ? C4::Context->userenv->{"surname"}      : "Inconnu",
457         emailaddress    => C4::Context->userenv ? C4::Context->userenv->{"emailaddress"} : undef,
458         TagsEnabled     => C4::Context->preference("TagsEnabled"),
459         hide_marc       => C4::Context->preference("hide_marc"),
460         item_level_itypes  => C4::Context->preference('item-level_itypes'),
461         patronimages       => C4::Context->preference("patronimages"),
462         singleBranchMode   => ( Koha::Libraries->search->count == 1 ),
463         XSLTDetailsDisplay => C4::Context->preference("XSLTDetailsDisplay"),
464         XSLTResultsDisplay => C4::Context->preference("XSLTResultsDisplay"),
465         using_https        => $using_https,
466         noItemTypeImages   => C4::Context->preference("noItemTypeImages"),
467         marcflavour        => C4::Context->preference("marcflavour"),
468         OPACBaseURL        => C4::Context->preference('OPACBaseURL'),
469         minPasswordLength  => $minPasswordLength,
470     );
471     if ( $in->{'type'} eq "intranet" ) {
472         $template->param(
473             AmazonCoverImages                                                          => C4::Context->preference("AmazonCoverImages"),
474             AutoLocation                                                               => C4::Context->preference("AutoLocation"),
475             "BiblioDefaultView" . C4::Context->preference("IntranetBiblioDefaultView") => 1,
476             CircAutocompl                                                              => C4::Context->preference("CircAutocompl"),
477             FRBRizeEditions                                                            => C4::Context->preference("FRBRizeEditions"),
478             IndependentBranches                                                        => C4::Context->preference("IndependentBranches"),
479             IntranetNav                                                                => C4::Context->preference("IntranetNav"),
480             IntranetmainUserblock                                                      => C4::Context->preference("IntranetmainUserblock"),
481             LibraryName                                                                => C4::Context->preference("LibraryName"),
482             LoginBranchname                                                            => ( C4::Context->userenv ? C4::Context->userenv->{"branchname"} : undef ),
483             advancedMARCEditor                                                         => C4::Context->preference("advancedMARCEditor"),
484             canreservefromotherbranches                                                => C4::Context->preference('canreservefromotherbranches'),
485             intranetcolorstylesheet                                                    => C4::Context->preference("intranetcolorstylesheet"),
486             IntranetFavicon                                                            => C4::Context->preference("IntranetFavicon"),
487             intranetreadinghistory                                                     => C4::Context->preference("intranetreadinghistory"),
488             intranetstylesheet                                                         => C4::Context->preference("intranetstylesheet"),
489             IntranetUserCSS                                                            => C4::Context->preference("IntranetUserCSS"),
490             IntranetUserJS                                                             => C4::Context->preference("IntranetUserJS"),
491             intranetbookbag                                                            => C4::Context->preference("intranetbookbag"),
492             suggestion                                                                 => C4::Context->preference("suggestion"),
493             virtualshelves                                                             => C4::Context->preference("virtualshelves"),
494             StaffSerialIssueDisplayCount                                               => C4::Context->preference("StaffSerialIssueDisplayCount"),
495             EasyAnalyticalRecords                                                      => C4::Context->preference('EasyAnalyticalRecords'),
496             LocalCoverImages                                                           => C4::Context->preference('LocalCoverImages'),
497             OPACLocalCoverImages                                                       => C4::Context->preference('OPACLocalCoverImages'),
498             AllowMultipleCovers                                                        => C4::Context->preference('AllowMultipleCovers'),
499             EnableBorrowerFiles                                                        => C4::Context->preference('EnableBorrowerFiles'),
500             UseKohaPlugins                                                             => C4::Context->preference('UseKohaPlugins'),
501             UseCourseReserves                                                          => C4::Context->preference("UseCourseReserves"),
502             useDischarge                                                               => C4::Context->preference('useDischarge'),
503             KOHA_VERSION                                                               => C4::Context->preference('Version'),
504         );
505     }
506     else {
507         warn "template type should be OPAC, here it is=[" . $in->{'type'} . "]" unless ( $in->{'type'} eq 'opac' );
508
509         #TODO : replace LibraryName syspref with 'system name', and remove this html processing
510         my $LibraryNameTitle = C4::Context->preference("LibraryName");
511         $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
512         $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
513
514         # clean up the busc param in the session
515         # if the page is not opac-detail and not the "add to list" page
516         # and not the "edit comments" page
517         if ( C4::Context->preference("OpacBrowseResults")
518             && $in->{'template_name'} =~ /opac-(.+)\.(?:tt|tmpl)$/ ) {
519             my $pagename = $1;
520             unless ( $pagename =~ /^(?:MARC|ISBD)?detail$/
521                 or $pagename =~ /^addbybiblionumber$/
522                 or $pagename =~ /^review$/ ) {
523                 my $sessionSearch = get_session( $sessionID || $in->{'query'}->cookie("CGISESSID") );
524                 $sessionSearch->clear( ["busc"] ) if ( $sessionSearch->param("busc") );
525             }
526         }
527
528         # variables passed from CGI: opac_css_override and opac_search_limits.
529         my $opac_search_limit   = $ENV{'OPAC_SEARCH_LIMIT'};
530         my $opac_limit_override = $ENV{'OPAC_LIMIT_OVERRIDE'};
531         my $opac_name           = '';
532         if (
533             ( $opac_limit_override && $opac_search_limit && $opac_search_limit =~ /branch:(\w+)/ ) ||
534             ( $in->{'query'}->param('limit') && $in->{'query'}->param('limit') =~ /branch:(\w+)/ ) ||
535             ( $in->{'query'}->param('multibranchlimit') && $in->{'query'}->param('multibranchlimit') =~ /multibranchlimit-(\w+)/ )
536           ) {
537             $opac_name = $1;    # opac_search_limit is a branch, so we use it.
538         } elsif ( $in->{'query'}->param('multibranchlimit') ) {
539             $opac_name = $in->{'query'}->param('multibranchlimit');
540         } elsif ( C4::Context->preference("SearchMyLibraryFirst") && C4::Context->userenv && C4::Context->userenv->{'branch'} ) {
541             $opac_name = C4::Context->userenv->{'branch'};
542         }
543
544         my @search_groups = Koha::Library::Groups->get_search_groups({ interface => 'opac' });
545         $template->param(
546             OpacAdditionalStylesheet                   => C4::Context->preference("OpacAdditionalStylesheet"),
547             AnonSuggestions                       => "" . C4::Context->preference("AnonSuggestions"),
548             LibrarySearchGroups                   => \@search_groups,
549             opac_name                             => $opac_name,
550             LibraryName                           => "" . C4::Context->preference("LibraryName"),
551             LibraryNameTitle                      => "" . $LibraryNameTitle,
552             LoginBranchname                       => C4::Context->userenv ? C4::Context->userenv->{"branchname"} : "",
553             OPACAmazonCoverImages                 => C4::Context->preference("OPACAmazonCoverImages"),
554             OPACFRBRizeEditions                   => C4::Context->preference("OPACFRBRizeEditions"),
555             OpacHighlightedWords                  => C4::Context->preference("OpacHighlightedWords"),
556             OPACShelfBrowser                      => "" . C4::Context->preference("OPACShelfBrowser"),
557             OPACURLOpenInNewWindow                => "" . C4::Context->preference("OPACURLOpenInNewWindow"),
558             OPACUserCSS                           => "" . C4::Context->preference("OPACUserCSS"),
559             OpacAuthorities                       => C4::Context->preference("OpacAuthorities"),
560             opac_css_override                     => $ENV{'OPAC_CSS_OVERRIDE'},
561             opac_search_limit                     => $opac_search_limit,
562             opac_limit_override                   => $opac_limit_override,
563             OpacBrowser                           => C4::Context->preference("OpacBrowser"),
564             OpacCloud                             => C4::Context->preference("OpacCloud"),
565             OpacKohaUrl                           => C4::Context->preference("OpacKohaUrl"),
566             OpacMainUserBlock                     => "" . C4::Context->preference("OpacMainUserBlock"),
567             OpacNav                               => "" . C4::Context->preference("OpacNav"),
568             OpacNavRight                          => "" . C4::Context->preference("OpacNavRight"),
569             OpacNavBottom                         => "" . C4::Context->preference("OpacNavBottom"),
570             OpacPasswordChange                    => C4::Context->preference("OpacPasswordChange"),
571             OPACPatronDetails                     => C4::Context->preference("OPACPatronDetails"),
572             OPACPrivacy                           => C4::Context->preference("OPACPrivacy"),
573             OPACFinesTab                          => C4::Context->preference("OPACFinesTab"),
574             OpacTopissue                          => C4::Context->preference("OpacTopissue"),
575             RequestOnOpac                         => C4::Context->preference("RequestOnOpac"),
576             'Version'                             => C4::Context->preference('Version'),
577             KOHA_VERSION                          => C4::Context->preference('Version'),
578             hidelostitems                         => C4::Context->preference("hidelostitems"),
579             mylibraryfirst                        => ( C4::Context->preference("SearchMyLibraryFirst") && C4::Context->userenv ) ? C4::Context->userenv->{'branch'} : '',
580             opaclayoutstylesheet                  => "" . C4::Context->preference("opaclayoutstylesheet"),
581             opacbookbag                           => "" . C4::Context->preference("opacbookbag"),
582             opaccredits                           => "" . C4::Context->preference("opaccredits"),
583             OpacFavicon                           => C4::Context->preference("OpacFavicon"),
584             opacheader                            => "" . C4::Context->preference("opacheader"),
585             opaclanguagesdisplay                  => "" . C4::Context->preference("opaclanguagesdisplay"),
586             opacreadinghistory                    => C4::Context->preference("opacreadinghistory"),
587             OPACUserJS                            => C4::Context->preference("OPACUserJS"),
588             opacuserlogin                         => "" . C4::Context->preference("opacuserlogin"),
589             OpenLibrarySearch                     => C4::Context->preference("OpenLibrarySearch"),
590             ShowReviewer                          => C4::Context->preference("ShowReviewer"),
591             ShowReviewerPhoto                     => C4::Context->preference("ShowReviewerPhoto"),
592             suggestion                            => "" . C4::Context->preference("suggestion"),
593             virtualshelves                        => "" . C4::Context->preference("virtualshelves"),
594             OPACSerialIssueDisplayCount           => C4::Context->preference("OPACSerialIssueDisplayCount"),
595             OPACXSLTDetailsDisplay                => C4::Context->preference("OPACXSLTDetailsDisplay"),
596             OPACXSLTResultsDisplay                => C4::Context->preference("OPACXSLTResultsDisplay"),
597             SyndeticsClientCode                   => C4::Context->preference("SyndeticsClientCode"),
598             SyndeticsEnabled                      => C4::Context->preference("SyndeticsEnabled"),
599             SyndeticsCoverImages                  => C4::Context->preference("SyndeticsCoverImages"),
600             SyndeticsTOC                          => C4::Context->preference("SyndeticsTOC"),
601             SyndeticsSummary                      => C4::Context->preference("SyndeticsSummary"),
602             SyndeticsEditions                     => C4::Context->preference("SyndeticsEditions"),
603             SyndeticsExcerpt                      => C4::Context->preference("SyndeticsExcerpt"),
604             SyndeticsReviews                      => C4::Context->preference("SyndeticsReviews"),
605             SyndeticsAuthorNotes                  => C4::Context->preference("SyndeticsAuthorNotes"),
606             SyndeticsAwards                       => C4::Context->preference("SyndeticsAwards"),
607             SyndeticsSeries                       => C4::Context->preference("SyndeticsSeries"),
608             SyndeticsCoverImageSize               => C4::Context->preference("SyndeticsCoverImageSize"),
609             OPACLocalCoverImages                  => C4::Context->preference("OPACLocalCoverImages"),
610             PatronSelfRegistration                => C4::Context->preference("PatronSelfRegistration"),
611             PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"),
612             useDischarge                 => C4::Context->preference('useDischarge'),
613         );
614
615         $template->param( OpacPublic => '1' ) if ( $user || C4::Context->preference("OpacPublic") );
616     }
617
618     # Check if we were asked using parameters to force a specific language
619     if ( defined $in->{'query'}->param('language') ) {
620
621         # Extract the language, let C4::Languages::getlanguage choose
622         # what to do
623         my $language = C4::Languages::getlanguage( $in->{'query'} );
624         my $languagecookie = C4::Templates::getlanguagecookie( $in->{'query'}, $language );
625         if ( ref $cookie eq 'ARRAY' ) {
626             push @{$cookie}, $languagecookie;
627         } else {
628             $cookie = [ $cookie, $languagecookie ];
629         }
630     }
631
632     return ( $template, $borrowernumber, $cookie, $flags );
633 }
634
635 =head2 checkauth
636
637   ($userid, $cookie, $sessionID) = &checkauth($query, $noauth, $flagsrequired, $type);
638
639 Verifies that the user is authorized to run this script.  If
640 the user is authorized, a (userid, cookie, session-id, flags)
641 quadruple is returned.  If the user is not authorized but does
642 not have the required privilege (see $flagsrequired below), it
643 displays an error page and exits.  Otherwise, it displays the
644 login page and exits.
645
646 Note that C<&checkauth> will return if and only if the user
647 is authorized, so it should be called early on, before any
648 unfinished operations (e.g., if you've opened a file, then
649 C<&checkauth> won't close it for you).
650
651 C<$query> is the CGI object for the script calling C<&checkauth>.
652
653 The C<$noauth> argument is optional. If it is set, then no
654 authorization is required for the script.
655
656 C<&checkauth> fetches user and session information from C<$query> and
657 ensures that the user is authorized to run scripts that require
658 authorization.
659
660 The C<$flagsrequired> argument specifies the required privileges
661 the user must have if the username and password are correct.
662 It should be specified as a reference-to-hash; keys in the hash
663 should be the "flags" for the user, as specified in the Members
664 intranet module. Any key specified must correspond to a "flag"
665 in the userflags table. E.g., { circulate => 1 } would specify
666 that the user must have the "circulate" privilege in order to
667 proceed. To make sure that access control is correct, the
668 C<$flagsrequired> parameter must be specified correctly.
669
670 Koha also has a concept of sub-permissions, also known as
671 granular permissions.  This makes the value of each key
672 in the C<flagsrequired> hash take on an additional
673 meaning, i.e.,
674
675  1
676
677 The user must have access to all subfunctions of the module
678 specified by the hash key.
679
680  *
681
682 The user must have access to at least one subfunction of the module
683 specified by the hash key.
684
685  specific permission, e.g., 'export_catalog'
686
687 The user must have access to the specific subfunction list, which
688 must correspond to a row in the permissions table.
689
690 The C<$type> argument specifies whether the template should be
691 retrieved from the opac or intranet directory tree.  "opac" is
692 assumed if it is not specified; however, if C<$type> is specified,
693 "intranet" is assumed if it is not "opac".
694
695 If C<$query> does not have a valid session ID associated with it
696 (i.e., the user has not logged in) or if the session has expired,
697 C<&checkauth> presents the user with a login page (from the point of
698 view of the original script, C<&checkauth> does not return). Once the
699 user has authenticated, C<&checkauth> restarts the original script
700 (this time, C<&checkauth> returns).
701
702 The login page is provided using a HTML::Template, which is set in the
703 systempreferences table or at the top of this file. The variable C<$type>
704 selects which template to use, either the opac or the intranet
705 authentification template.
706
707 C<&checkauth> returns a user ID, a cookie, and a session ID. The
708 cookie should be sent back to the browser; it verifies that the user
709 has authenticated.
710
711 =cut
712
713 sub _version_check {
714     my $type  = shift;
715     my $query = shift;
716     my $version;
717
718     # If version syspref is unavailable, it means Koha is being installed,
719     # and so we must redirect to OPAC maintenance page or to the WebInstaller
720     # also, if OpacMaintenance is ON, OPAC should redirect to maintenance
721     if ( C4::Context->preference('OpacMaintenance') && $type eq 'opac' ) {
722         warn "OPAC Install required, redirecting to maintenance";
723         print $query->redirect("/cgi-bin/koha/maintenance.pl");
724         safe_exit;
725     }
726     unless ( $version = C4::Context->preference('Version') ) {    # assignment, not comparison
727         if ( $type ne 'opac' ) {
728             warn "Install required, redirecting to Installer";
729             print $query->redirect("/cgi-bin/koha/installer/install.pl");
730         } else {
731             warn "OPAC Install required, redirecting to maintenance";
732             print $query->redirect("/cgi-bin/koha/maintenance.pl");
733         }
734         safe_exit;
735     }
736
737     # check that database and koha version are the same
738     # there is no DB version, it's a fresh install,
739     # go to web installer
740     # there is a DB version, compare it to the code version
741     my $kohaversion = Koha::version();
742
743     # remove the 3 last . to have a Perl number
744     $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
745     $debug and print STDERR "kohaversion : $kohaversion\n";
746     if ( $version < $kohaversion ) {
747         my $warning = "Database update needed, redirecting to %s. Database is $version and Koha is $kohaversion";
748         if ( $type ne 'opac' ) {
749             warn sprintf( $warning, 'Installer' );
750             print $query->redirect("/cgi-bin/koha/installer/install.pl?step=1&op=updatestructure");
751         } else {
752             warn sprintf( "OPAC: " . $warning, 'maintenance' );
753             print $query->redirect("/cgi-bin/koha/maintenance.pl");
754         }
755         safe_exit;
756     }
757 }
758
759 sub _session_log {
760     (@_) or return 0;
761     open my $fh, '>>', "/tmp/sessionlog" or warn "ERROR: Cannot append to /tmp/sessionlog";
762     printf $fh join( "\n", @_ );
763     close $fh;
764 }
765
766 sub _timeout_syspref {
767     my $timeout = C4::Context->preference('timeout') || 600;
768
769     # value in days, convert in seconds
770     if ( $timeout =~ /(\d+)[dD]/ ) {
771         $timeout = $1 * 86400;
772     }
773     return $timeout;
774 }
775
776 sub checkauth {
777     my $query = shift;
778     $debug and warn "Checking Auth";
779     # $authnotrequired will be set for scripts which will run without authentication
780     my $authnotrequired = shift;
781     my $flagsrequired   = shift;
782     my $type            = shift;
783     my $emailaddress    = shift;
784     $type = 'opac' unless $type;
785
786     my $dbh     = C4::Context->dbh;
787     my $timeout = _timeout_syspref();
788
789     _version_check( $type, $query );
790
791     # state variables
792     my $loggedin = 0;
793     my %info;
794     my ( $userid, $cookie, $sessionID, $flags );
795     my $logout = $query->param('logout.x');
796
797     my $anon_search_history;
798     my $cas_ticket = '';
799     # This parameter is the name of the CAS server we want to authenticate against,
800     # when using authentication against multiple CAS servers, as configured in Auth_cas_servers.yaml
801     my $casparam = $query->param('cas');
802     my $q_userid = $query->param('userid') // '';
803
804     # Basic authentication is incompatible with the use of Shibboleth,
805     # as Shibboleth may return REMOTE_USER as a Shibboleth attribute,
806     # and it may not be the attribute we want to use to match the koha login.
807     #
808     # Also, do not consider an empty REMOTE_USER.
809     #
810     # Finally, after those tests, we can assume (although if it would be better with
811     # a syspref) that if we get a REMOTE_USER, that's from basic authentication,
812     # and we can affect it to $userid.
813     if ( !$shib and defined( $ENV{'REMOTE_USER'} ) and $ENV{'REMOTE_USER'} ne '' and $userid = $ENV{'REMOTE_USER'} ) {
814
815         # Using Basic Authentication, no cookies required
816         $cookie = $query->cookie(
817             -name     => 'CGISESSID',
818             -value    => '',
819             -expires  => '',
820             -HttpOnly => 1,
821         );
822         $loggedin = 1;
823     }
824     elsif ( $emailaddress) {
825         # the Google OpenID Connect passes an email address
826     }
827     elsif ( $sessionID = $query->cookie("CGISESSID") )
828     {    # assignment, not comparison
829         my $session = get_session($sessionID);
830         C4::Context->_new_userenv($sessionID);
831         my ( $ip, $lasttime, $sessiontype );
832         my $s_userid = '';
833         if ($session) {
834             $s_userid = $session->param('id') // '';
835             C4::Context->set_userenv(
836                 $session->param('number'),       $s_userid,
837                 $session->param('cardnumber'),   $session->param('firstname'),
838                 $session->param('surname'),      $session->param('branch'),
839                 $session->param('branchname'),   $session->param('flags'),
840                 $session->param('emailaddress'), $session->param('branchprinter'),
841                 $session->param('shibboleth')
842             );
843             C4::Context::set_shelves_userenv( 'bar', $session->param('barshelves') );
844             C4::Context::set_shelves_userenv( 'pub', $session->param('pubshelves') );
845             C4::Context::set_shelves_userenv( 'tot', $session->param('totshelves') );
846             $debug and printf STDERR "AUTH_SESSION: (%s)\t%s %s - %s\n", map { $session->param($_) } qw(cardnumber firstname surname branch);
847             $ip          = $session->param('ip');
848             $lasttime    = $session->param('lasttime');
849             $userid      = $s_userid;
850             $sessiontype = $session->param('sessiontype') || '';
851         }
852         if ( ( $query->param('koha_login_context') && ( $q_userid ne $s_userid ) )
853             || ( $cas && $query->param('ticket') && !C4::Context->userenv->{'id'} )
854             || ( $shib && $shib_login && !$logout && !C4::Context->userenv->{'id'} )
855         ) {
856
857             #if a user enters an id ne to the id in the current session, we need to log them in...
858             #first we need to clear the anonymous session...
859             $debug and warn "query id = $q_userid but session id = $s_userid";
860             $anon_search_history = $session->param('search_history');
861             $session->delete();
862             $session->flush;
863             C4::Context->_unset_userenv($sessionID);
864             $sessionID = undef;
865             $userid    = undef;
866         }
867         elsif ($logout) {
868
869             # voluntary logout the user
870             # check wether the user was using their shibboleth session or a local one
871             my $shibSuccess = C4::Context->userenv->{'shibboleth'};
872             $session->delete();
873             $session->flush;
874             C4::Context->_unset_userenv($sessionID);
875
876             #_session_log(sprintf "%20s from %16s logged out at %30s (manually).\n", $userid,$ip,(strftime "%c",localtime));
877             $sessionID = undef;
878             $userid    = undef;
879
880             if ($cas and $caslogout) {
881                 logout_cas($query, $type);
882             }
883
884             # If we are in a shibboleth session (shibboleth is enabled, a shibboleth match attribute is set and matches koha matchpoint)
885             if ( $shib and $shib_login and $shibSuccess and $type eq 'opac' ) {
886
887                 # (Note: $type eq 'opac' condition should be removed when shibboleth authentication for intranet will be implemented)
888                 logout_shib($query);
889             }
890         }
891         elsif ( !$lasttime || ( $lasttime < time() - $timeout ) ) {
892
893             # timed logout
894             $info{'timed_out'} = 1;
895             if ($session) {
896                 $session->delete();
897                 $session->flush;
898             }
899             C4::Context->_unset_userenv($sessionID);
900
901             #_session_log(sprintf "%20s from %16s logged out at %30s (inactivity).\n", $userid,$ip,(strftime "%c",localtime));
902             $userid    = undef;
903             $sessionID = undef;
904         }
905         elsif ( C4::Context->preference('SessionRestrictionByIP') && $ip ne $ENV{'REMOTE_ADDR'} ) {
906
907             # Different ip than originally logged in from
908             $info{'oldip'}        = $ip;
909             $info{'newip'}        = $ENV{'REMOTE_ADDR'};
910             $info{'different_ip'} = 1;
911             $session->delete();
912             $session->flush;
913             C4::Context->_unset_userenv($sessionID);
914
915             #_session_log(sprintf "%20s from %16s logged out at %30s (ip changed to %16s).\n", $userid,$ip,(strftime "%c",localtime), $info{'newip'});
916             $sessionID = undef;
917             $userid    = undef;
918         }
919         else {
920             $cookie = $query->cookie(
921                 -name     => 'CGISESSID',
922                 -value    => $session->id,
923                 -HttpOnly => 1
924             );
925             $session->param( 'lasttime', time() );
926             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...
927                 $flags = haspermission( $userid, $flagsrequired );
928                 if ($flags) {
929                     $loggedin = 1;
930                 } else {
931                     $info{'nopermission'} = 1;
932                 }
933             }
934         }
935     }
936     unless ( $userid || $sessionID ) {
937         #we initiate a session prior to checking for a username to allow for anonymous sessions...
938         my $session = get_session("") or die "Auth ERROR: Cannot get_session()";
939
940         # Save anonymous search history in new session so it can be retrieved
941         # by get_template_and_user to store it in user's search history after
942         # a successful login.
943         if ($anon_search_history) {
944             $session->param( 'search_history', $anon_search_history );
945         }
946
947         my $sessionID = $session->id;
948         C4::Context->_new_userenv($sessionID);
949         $cookie = $query->cookie(
950             -name     => 'CGISESSID',
951             -value    => $session->id,
952             -HttpOnly => 1
953         );
954         my $pki_field = C4::Context->preference('AllowPKIAuth');
955         if ( !defined($pki_field) ) {
956             print STDERR "ERROR: Missing system preference AllowPKIAuth.\n";
957             $pki_field = 'None';
958         }
959         if ( ( $cas && $query->param('ticket') )
960             || $q_userid
961             || ( $shib && $shib_login )
962             || $pki_field ne 'None'
963             || $emailaddress )
964         {
965             my $password    = $query->param('password');
966             my $shibSuccess = 0;
967             my ( $return, $cardnumber );
968
969             # If shib is enabled and we have a shib login, does the login match a valid koha user
970             if ( $shib && $shib_login && $type eq 'opac' ) {
971                 my $retuserid;
972
973                 # Do not pass password here, else shib will not be checked in checkpw.
974                 ( $return, $cardnumber, $retuserid ) = checkpw( $dbh, $q_userid, undef, $query );
975                 $userid      = $retuserid;
976                 $shibSuccess = $return;
977                 $info{'invalidShibLogin'} = 1 unless ($return);
978             }
979
980             # If shib login and match were successful, skip further login methods
981             unless ($shibSuccess) {
982                 if ( $cas && $query->param('ticket') ) {
983                     my $retuserid;
984                     ( $return, $cardnumber, $retuserid, $cas_ticket ) =
985                       checkpw( $dbh, $userid, $password, $query, $type );
986                     $userid = $retuserid;
987                     $info{'invalidCasLogin'} = 1 unless ($return);
988                 }
989
990                 elsif ( $emailaddress ) {
991                     my $value = $emailaddress;
992
993                     # If we're looking up the email, there's a chance that the person
994                     # doesn't have a userid. So if there is none, we pass along the
995                     # borrower number, and the bits of code that need to know the user
996                     # ID will have to be smart enough to handle that.
997                     my $patrons = Koha::Patrons->search({ email => $value });
998                     if ($patrons->count) {
999
1000                         # First the userid, then the borrowernum
1001                         my $patron = $patrons->next;
1002                         $value = $patron->userid || $patron->borrowernumber;
1003                     } else {
1004                         undef $value;
1005                     }
1006                     $return = $value ? 1 : 0;
1007                     $userid = $value;
1008                 }
1009
1010                 elsif (
1011                     ( $pki_field eq 'Common Name' && $ENV{'SSL_CLIENT_S_DN_CN'} )
1012                     || ( $pki_field eq 'emailAddress'
1013                         && $ENV{'SSL_CLIENT_S_DN_Email'} )
1014                   )
1015                 {
1016                     my $value;
1017                     if ( $pki_field eq 'Common Name' ) {
1018                         $value = $ENV{'SSL_CLIENT_S_DN_CN'};
1019                     }
1020                     elsif ( $pki_field eq 'emailAddress' ) {
1021                         $value = $ENV{'SSL_CLIENT_S_DN_Email'};
1022
1023                         # If we're looking up the email, there's a chance that the person
1024                         # doesn't have a userid. So if there is none, we pass along the
1025                         # borrower number, and the bits of code that need to know the user
1026                         # ID will have to be smart enough to handle that.
1027                         my $patrons = Koha::Patrons->search({ email => $value });
1028                         if ($patrons->count) {
1029
1030                             # First the userid, then the borrowernum
1031                             my $patron = $patrons->next;
1032                             $value = $patron->userid || $patron->borrowernumber;
1033                         } else {
1034                             undef $value;
1035                         }
1036                     }
1037
1038                     $return = $value ? 1 : 0;
1039                     $userid = $value;
1040
1041                 }
1042                 else {
1043                     my $retuserid;
1044                     ( $return, $cardnumber, $retuserid, $cas_ticket ) =
1045                       checkpw( $dbh, $q_userid, $password, $query, $type );
1046                     $userid = $retuserid if ($retuserid);
1047                     $info{'invalid_username_or_password'} = 1 unless ($return);
1048                 }
1049             }
1050
1051             # $return: 1 = valid user, 2 = superlibrarian
1052             if ($return) {
1053                 # If DB user is logged in
1054                 $userid ||= $q_userid if $return == 2;
1055
1056                 #_session_log(sprintf "%20s from %16s logged in  at %30s.\n", $userid,$ENV{'REMOTE_ADDR'},(strftime '%c', localtime));
1057                 if ( $flags = haspermission( $userid, $flagsrequired ) ) {
1058                     $loggedin = 1;
1059                 }
1060                 else {
1061                     $info{'nopermission'} = 1;
1062                     C4::Context->_unset_userenv($sessionID);
1063                 }
1064                 my ( $borrowernumber, $firstname, $surname, $userflags,
1065                     $branchcode, $branchname, $branchprinter, $emailaddress );
1066
1067                 if ( $return == 1 ) {
1068                     my $select = "
1069                     SELECT borrowernumber, firstname, surname, flags, borrowers.branchcode,
1070                     branches.branchname    as branchname,
1071                     branches.branchprinter as branchprinter,
1072                     email
1073                     FROM borrowers
1074                     LEFT JOIN branches on borrowers.branchcode=branches.branchcode
1075                     ";
1076                     my $sth = $dbh->prepare("$select where userid=?");
1077                     $sth->execute($userid);
1078                     unless ( $sth->rows ) {
1079                         $debug and print STDERR "AUTH_1: no rows for userid='$userid'\n";
1080                         $sth = $dbh->prepare("$select where cardnumber=?");
1081                         $sth->execute($cardnumber);
1082
1083                         unless ( $sth->rows ) {
1084                             $debug and print STDERR "AUTH_2a: no rows for cardnumber='$cardnumber'\n";
1085                             $sth->execute($userid);
1086                             unless ( $sth->rows ) {
1087                                 $debug and print STDERR "AUTH_2b: no rows for userid='$userid' AS cardnumber\n";
1088                             }
1089                         }
1090                     }
1091                     if ( $sth->rows ) {
1092                         ( $borrowernumber, $firstname, $surname, $userflags,
1093                             $branchcode, $branchname, $branchprinter, $emailaddress ) = $sth->fetchrow;
1094                         $debug and print STDERR "AUTH_3 results: " .
1095                           "$cardnumber,$borrowernumber,$userid,$firstname,$surname,$userflags,$branchcode,$emailaddress\n";
1096                     } else {
1097                         print STDERR "AUTH_3: no results for userid='$userid', cardnumber='$cardnumber'.\n";
1098                     }
1099
1100                     # launch a sequence to check if we have a ip for the branch, i
1101                     # if we have one we replace the branchcode of the userenv by the branch bound in the ip.
1102
1103                     my $ip = $ENV{'REMOTE_ADDR'};
1104
1105                     # if they specify at login, use that
1106                     if ( $query->param('branch') ) {
1107                         $branchcode = $query->param('branch');
1108                         my $library = Koha::Libraries->find($branchcode);
1109                         $branchname = $library? $library->branchname: '';
1110                     }
1111                     my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search };
1112                     if ( $type ne 'opac' and C4::Context->boolean_preference('AutoLocation') ) {
1113
1114                         # we have to check they are coming from the right ip range
1115                         my $domain = $branches->{$branchcode}->{'branchip'};
1116                         $domain =~ s|\.\*||g;
1117                         if ( $ip !~ /^$domain/ ) {
1118                             $loggedin = 0;
1119                             $cookie = $query->cookie(
1120                                 -name     => 'CGISESSID',
1121                                 -value    => '',
1122                                 -HttpOnly => 1
1123                             );
1124                             $info{'wrongip'} = 1;
1125                         }
1126                     }
1127
1128                     foreach my $br ( keys %$branches ) {
1129
1130                         #     now we work with the treatment of ip
1131                         my $domain = $branches->{$br}->{'branchip'};
1132                         if ( $domain && $ip =~ /^$domain/ ) {
1133                             $branchcode = $branches->{$br}->{'branchcode'};
1134
1135                             # new op dev : add the branchprinter and branchname in the cookie
1136                             $branchprinter = $branches->{$br}->{'branchprinter'};
1137                             $branchname    = $branches->{$br}->{'branchname'};
1138                         }
1139                     }
1140                     $session->param( 'number',       $borrowernumber );
1141                     $session->param( 'id',           $userid );
1142                     $session->param( 'cardnumber',   $cardnumber );
1143                     $session->param( 'firstname',    $firstname );
1144                     $session->param( 'surname',      $surname );
1145                     $session->param( 'branch',       $branchcode );
1146                     $session->param( 'branchname',   $branchname );
1147                     $session->param( 'flags',        $userflags );
1148                     $session->param( 'emailaddress', $emailaddress );
1149                     $session->param( 'ip',           $session->remote_addr() );
1150                     $session->param( 'lasttime',     time() );
1151                     $session->param( 'shibboleth',   $shibSuccess );
1152                     $debug and printf STDERR "AUTH_4: (%s)\t%s %s - %s\n", map { $session->param($_) } qw(cardnumber firstname surname branch);
1153                 }
1154                 elsif ( $return == 2 ) {
1155
1156                     #We suppose the user is the superlibrarian
1157                     $borrowernumber = 0;
1158                     $session->param( 'number',       0 );
1159                     $session->param( 'id',           C4::Context->config('user') );
1160                     $session->param( 'cardnumber',   C4::Context->config('user') );
1161                     $session->param( 'firstname',    C4::Context->config('user') );
1162                     $session->param( 'surname',      C4::Context->config('user') );
1163                     $session->param( 'branch',       'NO_LIBRARY_SET' );
1164                     $session->param( 'branchname',   'NO_LIBRARY_SET' );
1165                     $session->param( 'flags',        1 );
1166                     $session->param( 'emailaddress', C4::Context->preference('KohaAdminEmailAddress') );
1167                     $session->param( 'ip',           $session->remote_addr() );
1168                     $session->param( 'lasttime',     time() );
1169                 }
1170                 $session->param('cas_ticket', $cas_ticket) if $cas_ticket;
1171                 C4::Context->set_userenv(
1172                     $session->param('number'),       $session->param('id'),
1173                     $session->param('cardnumber'),   $session->param('firstname'),
1174                     $session->param('surname'),      $session->param('branch'),
1175                     $session->param('branchname'),   $session->param('flags'),
1176                     $session->param('emailaddress'), $session->param('branchprinter'),
1177                     $session->param('shibboleth')
1178                 );
1179
1180             }
1181             # $return: 0 = invalid user
1182             # reset to anonymous session
1183             else {
1184                 $debug and warn "Login failed, resetting anonymous session...";
1185                 if ($userid) {
1186                     $info{'invalid_username_or_password'} = 1;
1187                     C4::Context->_unset_userenv($sessionID);
1188                 }
1189                 $session->param( 'lasttime', time() );
1190                 $session->param( 'ip',       $session->remote_addr() );
1191                 $session->param( 'sessiontype', 'anon' );
1192             }
1193         }    # END if ( $q_userid
1194         elsif ( $type eq "opac" ) {
1195
1196             # if we are here this is an anonymous session; add public lists to it and a few other items...
1197             # anonymous sessions are created only for the OPAC
1198             $debug and warn "Initiating an anonymous session...";
1199
1200             # setting a couple of other session vars...
1201             $session->param( 'ip',          $session->remote_addr() );
1202             $session->param( 'lasttime',    time() );
1203             $session->param( 'sessiontype', 'anon' );
1204         }
1205     }    # END unless ($userid)
1206
1207     # finished authentification, now respond
1208     if ( $loggedin || $authnotrequired )
1209     {
1210         # successful login
1211         unless ($cookie) {
1212             $cookie = $query->cookie(
1213                 -name     => 'CGISESSID',
1214                 -value    => '',
1215                 -HttpOnly => 1
1216             );
1217         }
1218
1219         if ( $userid ) {
1220             # track_login also depends on pref TrackLastPatronActivity
1221             my $patron = Koha::Patrons->find({ userid => $userid });
1222             $patron->track_login if $patron;
1223         }
1224
1225         return ( $userid, $cookie, $sessionID, $flags );
1226     }
1227
1228     #
1229     #
1230     # AUTH rejected, show the login/password template, after checking the DB.
1231     #
1232     #
1233
1234     # get the inputs from the incoming query
1235     my @inputs = ();
1236     foreach my $name ( param $query) {
1237         (next) if ( $name eq 'userid' || $name eq 'password' || $name eq 'ticket' );
1238         my $value = $query->param($name);
1239         push @inputs, { name => $name, value => $value };
1240     }
1241
1242     my $patron = Koha::Patrons->find({ userid => $q_userid }); # Not necessary logged in!
1243
1244     my $LibraryNameTitle = C4::Context->preference("LibraryName");
1245     $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
1246     $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
1247
1248     my $template_name = ( $type eq 'opac' ) ? 'opac-auth.tt' : 'auth.tt';
1249     my $template = C4::Templates::gettemplate( $template_name, $type, $query );
1250     $template->param(
1251         OpacAdditionalStylesheet                   => C4::Context->preference("OpacAdditionalStylesheet"),
1252         opaclayoutstylesheet                  => C4::Context->preference("opaclayoutstylesheet"),
1253         login                                 => 1,
1254         INPUTS                                => \@inputs,
1255         script_name                           => get_script_name(),
1256         casAuthentication                     => C4::Context->preference("casAuthentication"),
1257         shibbolethAuthentication              => $shib,
1258         SessionRestrictionByIP                => C4::Context->preference("SessionRestrictionByIP"),
1259         suggestion                            => C4::Context->preference("suggestion"),
1260         virtualshelves                        => C4::Context->preference("virtualshelves"),
1261         LibraryName                           => "" . C4::Context->preference("LibraryName"),
1262         LibraryNameTitle                      => "" . $LibraryNameTitle,
1263         opacuserlogin                         => C4::Context->preference("opacuserlogin"),
1264         OpacNav                               => C4::Context->preference("OpacNav"),
1265         OpacNavRight                          => C4::Context->preference("OpacNavRight"),
1266         OpacNavBottom                         => C4::Context->preference("OpacNavBottom"),
1267         opaccredits                           => C4::Context->preference("opaccredits"),
1268         OpacFavicon                           => C4::Context->preference("OpacFavicon"),
1269         opacreadinghistory                    => C4::Context->preference("opacreadinghistory"),
1270         opaclanguagesdisplay                  => C4::Context->preference("opaclanguagesdisplay"),
1271         OPACUserJS                            => C4::Context->preference("OPACUserJS"),
1272         opacbookbag                           => "" . C4::Context->preference("opacbookbag"),
1273         OpacCloud                             => C4::Context->preference("OpacCloud"),
1274         OpacTopissue                          => C4::Context->preference("OpacTopissue"),
1275         OpacAuthorities                       => C4::Context->preference("OpacAuthorities"),
1276         OpacBrowser                           => C4::Context->preference("OpacBrowser"),
1277         opacheader                            => C4::Context->preference("opacheader"),
1278         TagsEnabled                           => C4::Context->preference("TagsEnabled"),
1279         OPACUserCSS                           => C4::Context->preference("OPACUserCSS"),
1280         intranetcolorstylesheet               => C4::Context->preference("intranetcolorstylesheet"),
1281         intranetstylesheet                    => C4::Context->preference("intranetstylesheet"),
1282         intranetbookbag                       => C4::Context->preference("intranetbookbag"),
1283         IntranetNav                           => C4::Context->preference("IntranetNav"),
1284         IntranetFavicon                       => C4::Context->preference("IntranetFavicon"),
1285         IntranetUserCSS                       => C4::Context->preference("IntranetUserCSS"),
1286         IntranetUserJS                        => C4::Context->preference("IntranetUserJS"),
1287         IndependentBranches                   => C4::Context->preference("IndependentBranches"),
1288         AutoLocation                          => C4::Context->preference("AutoLocation"),
1289         wrongip                               => $info{'wrongip'},
1290         PatronSelfRegistration                => C4::Context->preference("PatronSelfRegistration"),
1291         PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"),
1292         opac_css_override                     => $ENV{'OPAC_CSS_OVERRIDE'},
1293         too_many_login_attempts               => ( $patron and $patron->account_locked ),
1294         KOHA_VERSION                          => C4::Context->preference('Version'),
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