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