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