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