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