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