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