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