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