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