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