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