bug 4896: granular permissions now always on (DB rev 138)
[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; FIXME - Bug 2505
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::Output;    # to get the template
30 use C4::Members;
31 use C4::Koha;
32 use C4::Branch; # GetBranches
33 use C4::VirtualShelves;
34 use POSIX qw/strftime/;
35
36 # use utf8;
37 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug $ldap $cas $caslogout);
38
39 BEGIN {
40     $VERSION = 3.02;        # set version for version checking
41     $debug = $ENV{DEBUG};
42     @ISA   = qw(Exporter);
43     @EXPORT    = qw(&checkauth &get_template_and_user &haspermission &get_user_subpermissions);
44     @EXPORT_OK = qw(&check_api_auth &get_session &check_cookie_auth &checkpw &get_all_subpermissions &get_user_subpermissions);
45     %EXPORT_TAGS = (EditPermissions => [qw(get_all_subpermissions get_user_subpermissions)]);
46     $ldap = C4::Context->config('useldapserver') || 0;
47     $cas = C4::Context->preference('casAuthentication');
48     $caslogout = C4::Context->preference('casLogout');
49     if ($ldap) {
50         require C4::Auth_with_ldap;             # no import
51         import  C4::Auth_with_ldap qw(checkpw_ldap);
52     }
53     if ($cas) {
54         require C4::Auth_with_cas;             # no import
55         import  C4::Auth_with_cas qw(checkpw_cas login_cas logout_cas login_cas_url);
56     }
57
58 }
59
60 =head1 NAME
61
62 C4::Auth - Authenticates Koha users
63
64 =head1 SYNOPSIS
65
66   use CGI;
67   use C4::Auth;
68   use C4::Output;
69
70   my $query = new CGI;
71
72   my ($template, $borrowernumber, $cookie)
73     = get_template_and_user(
74         {
75             template_name   => "opac-main.tmpl",
76             query           => $query,
77       type            => "opac",
78       authnotrequired => 1,
79       flagsrequired   => {borrow => 1, catalogue => '*', tools => 'import_patrons' },
80   }
81     );
82
83   output_html_with_http_headers $query, $cookie, $template->output;
84
85 =head1 DESCRIPTION
86
87 The main function of this module is to provide
88 authentification. However the get_template_and_user function has
89 been provided so that a users login information is passed along
90 automatically. This gets loaded into the template.
91
92 =head1 FUNCTIONS
93
94 =head2 get_template_and_user
95
96  my ($template, $borrowernumber, $cookie)
97      = get_template_and_user(
98        {
99          template_name   => "opac-main.tmpl",
100          query           => $query,
101          type            => "opac",
102          authnotrequired => 1,
103          flagsrequired   => {borrow => 1, catalogue => '*', tools => 'import_patrons' },
104        }
105      );
106
107 This call passes the C<query>, C<flagsrequired> and C<authnotrequired>
108 to C<&checkauth> (in this module) to perform authentification.
109 See C<&checkauth> for an explanation of these parameters.
110
111 The C<template_name> is then used to find the correct template for
112 the page. The authenticated users details are loaded onto the
113 template in the HTML::Template LOOP variable C<USER_INFO>. Also the
114 C<sessionID> is passed to the template. This can be used in templates
115 if cookies are disabled. It needs to be put as and input to every
116 authenticated page.
117
118 More information on the C<gettemplate> sub can be found in the
119 Output.pm module.
120
121 =cut
122
123 my $SEARCH_HISTORY_INSERT_SQL =<<EOQ;
124 INSERT INTO search_history(userid, sessionid, query_desc, query_cgi, total, time            )
125 VALUES                    (     ?,         ?,          ?,         ?,     ?, FROM_UNIXTIME(?))
126 EOQ
127 sub get_template_and_user {
128     my $in       = shift;
129     my $template =
130       gettemplate( $in->{'template_name'}, $in->{'type'}, $in->{'query'} );
131     my ( $user, $cookie, $sessionID, $flags ) = checkauth(
132         $in->{'query'},
133         $in->{'authnotrequired'},
134         $in->{'flagsrequired'},
135         $in->{'type'}
136     ) unless ($in->{'template_name'}=~/maintenance/);
137
138     my $borrowernumber;
139     my $insecure = C4::Context->preference('insecure');
140     if ($user or $insecure) {
141
142         # load the template variables for stylesheets and JavaScript
143         $template->param( css_libs => $in->{'css_libs'} );
144         $template->param( css_module => $in->{'css_module'} );
145         $template->param( css_page => $in->{'css_page'} );
146         $template->param( css_widgets => $in->{'css_widgets'} );
147
148         $template->param( js_libs => $in->{'js_libs'} );
149         $template->param( js_module => $in->{'js_module'} );
150         $template->param( js_page => $in->{'js_page'} );
151         $template->param( js_widgets => $in->{'js_widgets'} );
152
153         # user info
154         $template->param( loggedinusername => $user );
155         $template->param( sessionID        => $sessionID );
156
157                 my ($total, $pubshelves, $barshelves) = C4::Context->get_shelves_userenv();
158                 if (defined($pubshelves)) {
159                 $template->param(       pubshelves      => scalar (@$pubshelves),
160                                                         pubshelvesloop  => $pubshelves,
161                                                         );
162                         $template->param(       pubtotal                => $total->{'pubtotal'}, ) if ($total->{'pubtotal'} > scalar (@$pubshelves));
163                 }
164                 if (defined($barshelves)) {
165                 $template->param(       barshelves      => scalar (@$barshelves),
166                                                         barshelvesloop  => $barshelves,
167                                                         );
168                         $template->param(       bartotal                => $total->{'bartotal'}, ) if ($total->{'bartotal'} > scalar (@$barshelves));
169                 }
170
171         $borrowernumber = getborrowernumber($user) if defined($user);
172
173         my ( $borr ) = GetMemberDetails( $borrowernumber );
174         my @bordat;
175         $bordat[0] = $borr;
176         $template->param( "USER_INFO" => \@bordat );
177
178         my $all_perms = get_all_subpermissions();
179
180         my @flagroots = qw(circulate catalogue parameters borrowers permissions reserveforothers borrow
181                             editcatalogue updatecharges management tools editauthorities serials reports acquisition);
182         # We are going to use the $flags returned by checkauth
183         # to create the template's parameters that will indicate
184         # which menus the user can access.
185         if (( $flags && $flags->{superlibrarian}==1) or $insecure==1) {
186             $template->param( CAN_user_circulate        => 1 );
187             $template->param( CAN_user_catalogue        => 1 );
188             $template->param( CAN_user_parameters       => 1 );
189             $template->param( CAN_user_borrowers        => 1 );
190             $template->param( CAN_user_permissions      => 1 );
191             $template->param( CAN_user_reserveforothers => 1 );
192             $template->param( CAN_user_borrow           => 1 );
193             $template->param( CAN_user_editcatalogue    => 1 );
194             $template->param( CAN_user_updatecharges     => 1 );
195             $template->param( CAN_user_acquisition      => 1 );
196             $template->param( CAN_user_management       => 1 );
197             $template->param( CAN_user_tools            => 1 );
198             $template->param( CAN_user_editauthorities  => 1 );
199             $template->param( CAN_user_serials          => 1 );
200             $template->param( CAN_user_reports          => 1 );
201             $template->param( CAN_user_staffaccess      => 1 );
202             foreach my $module (keys %$all_perms) {
203                 foreach my $subperm (keys %{ $all_perms->{$module} }) {
204                     $template->param( "CAN_user_${module}_${subperm}" => 1 );
205                 }
206             }
207         }
208
209         if ( $flags ) {
210             foreach my $module (keys %$all_perms) {
211                 if ( $flags->{$module} == 1) {
212                     foreach my $subperm (keys %{ $all_perms->{$module} }) {
213                         $template->param( "CAN_user_${module}_${subperm}" => 1 );
214                     }
215                 } elsif ( ref($flags->{$module}) ) {
216                     foreach my $subperm (keys %{ $flags->{$module} } ) {
217                         $template->param( "CAN_user_${module}_${subperm}" => 1 );
218                     }
219                 }
220             }
221         }
222
223         if ($flags) {
224             foreach my $module (keys %$flags) {
225                 if ( $flags->{$module} == 1 or ref($flags->{$module}) ) {
226                     $template->param( "CAN_user_$module" => 1 );
227                     if ($module eq "parameters") {
228                         $template->param( CAN_user_management => 1 );
229                     }
230                 }
231             }
232         }
233                 # Logged-in opac search history
234                 # If the requested template is an opac one and opac search history is enabled
235                 if ($in->{'type'} == "opac" && C4::Context->preference('EnableOpacSearchHistory')) {
236                         my $dbh = C4::Context->dbh;
237                         my $query = "SELECT COUNT(*) FROM search_history WHERE userid=?";
238                         my $sth = $dbh->prepare($query);
239                         $sth->execute($borrowernumber);
240                         
241                         # If at least one search has already been performed
242                         if ($sth->fetchrow_array > 0) { 
243                         # We show the link in opac
244                         $template->param(ShowOpacRecentSearchLink => 1);
245                         }
246
247                         # And if there's a cookie with searches performed when the user was not logged in, 
248                         # we add them to the logged-in search history
249                         my $searchcookie = $in->{'query'}->cookie('KohaOpacRecentSearches');
250                         if ($searchcookie){
251                                 $searchcookie = uri_unescape($searchcookie);
252                                 my @recentSearches = @{thaw($searchcookie) || []};
253                                 if (@recentSearches) {
254                                         my $sth = $dbh->prepare($SEARCH_HISTORY_INSERT_SQL);
255                                         $sth->execute( $borrowernumber,
256                                                        $in->{'query'}->cookie("CGISESSID"),
257                                                        $_->{'query_desc'},
258                                                        $_->{'query_cgi'},
259                                                        $_->{'total'},
260                                                        $_->{'time'},
261                                         ) foreach @recentSearches;
262
263                                         # And then, delete the cookie's content
264                                         my $newsearchcookie = $in->{'query'}->cookie(
265                                                                                                 -name => 'KohaOpacRecentSearches',
266                                                                                                 -value => freeze([]),
267                                                                                                 -expires => ''
268                                                                                          );
269                                         $cookie = [$cookie, $newsearchcookie];
270                                 }
271                         }
272                 }
273     }
274         else {  # if this is an anonymous session, setup to display public lists...
275
276         # load the template variables for stylesheets and JavaScript
277         $template->param( css_libs => $in->{'css_libs'} );
278         $template->param( css_module => $in->{'css_module'} );
279         $template->param( css_page => $in->{'css_page'} );
280         $template->param( css_widgets => $in->{'css_widgets'} );
281
282         $template->param( js_libs => $in->{'js_libs'} );
283         $template->param( js_module => $in->{'js_module'} );
284         $template->param( js_page => $in->{'js_page'} );
285         $template->param( js_widgets => $in->{'js_widgets'} );
286
287         $template->param( sessionID        => $sessionID );
288         
289         my ($total, $pubshelves) = C4::Context->get_shelves_userenv();  # an anonymous user has no 'barshelves'...
290         if (defined(($pubshelves))) {
291             $template->param(   pubshelves      => scalar (@$pubshelves),
292                                 pubshelvesloop  => $pubshelves,
293                             );
294             $template->param(   pubtotal        => $total->{'pubtotal'}, ) if ($total->{'pubtotal'} > scalar (@$pubshelves));
295         }
296
297     }
298         # Anonymous opac search history
299         # If opac search history is enabled and at least one search has already been performed
300         if (C4::Context->preference('EnableOpacSearchHistory')) {
301                 my $searchcookie = $in->{'query'}->cookie('KohaOpacRecentSearches');
302                 if ($searchcookie){
303                         $searchcookie = uri_unescape($searchcookie);
304                         my @recentSearches = @{thaw($searchcookie) || []};
305             # We show the link in opac
306                         if (@recentSearches) {
307                                 $template->param(ShowOpacRecentSearchLink => 1);
308                         }
309             }
310         }
311
312     if(C4::Context->preference('dateformat')){
313         if(C4::Context->preference('dateformat') eq "metric"){
314             $template->param(dateformat_metric => 1);
315         } elsif(C4::Context->preference('dateformat') eq "us"){
316             $template->param(dateformat_us => 1);
317         } else {
318             $template->param(dateformat_iso => 1);
319         }
320     } else {
321         $template->param(dateformat_iso => 1);
322     }
323
324     # these template parameters are set the same regardless of $in->{'type'}
325     $template->param(
326             "BiblioDefaultView".C4::Context->preference("BiblioDefaultView")         => 1,
327             EnhancedMessagingPreferences => C4::Context->preference('EnhancedMessagingPreferences'),
328             GoogleJackets                => C4::Context->preference("GoogleJackets"),
329             KohaAdminEmailAddress        => "" . C4::Context->preference("KohaAdminEmailAddress"),
330             LoginBranchcode              => (C4::Context->userenv?C4::Context->userenv->{"branch"}:"insecure"),
331             LoginFirstname               => (C4::Context->userenv?C4::Context->userenv->{"firstname"}:"Bel"),
332             LoginSurname                 => C4::Context->userenv?C4::Context->userenv->{"surname"}:"Inconnu",
333             TagsEnabled                  => C4::Context->preference("TagsEnabled"),
334             hide_marc                    => C4::Context->preference("hide_marc"),
335             'item-level_itypes'          => C4::Context->preference('item-level_itypes'),
336             patronimages                 => C4::Context->preference("patronimages"),
337             singleBranchMode             => C4::Context->preference("singleBranchMode"),
338             XSLTDetailsDisplay           => C4::Context->preference("XSLTDetailsDisplay"),
339             XSLTResultsDisplay           => C4::Context->preference("XSLTResultsDisplay"),
340             BranchesLoop                 => GetBranchesLoop(),
341             using_https                  => $in->{'query'}->https() ? 1 : 0,
342     );
343
344     if ( $in->{'type'} eq "intranet" ) {
345         $template->param(
346             AmazonContent               => C4::Context->preference("AmazonContent"),
347             AmazonCoverImages           => C4::Context->preference("AmazonCoverImages"),
348             AmazonEnabled               => C4::Context->preference("AmazonEnabled"),
349             AmazonSimilarItems          => C4::Context->preference("AmazonSimilarItems"),
350             AutoLocation                => C4::Context->preference("AutoLocation"),
351             "BiblioDefaultView".C4::Context->preference("IntranetBiblioDefaultView") => 1,
352             CircAutocompl               => C4::Context->preference("CircAutocompl"),
353             FRBRizeEditions             => C4::Context->preference("FRBRizeEditions"),
354             IndependantBranches         => C4::Context->preference("IndependantBranches"),
355             IntranetNav                 => C4::Context->preference("IntranetNav"),
356             IntranetmainUserblock       => C4::Context->preference("IntranetmainUserblock"),
357             LibraryName                 => C4::Context->preference("LibraryName"),
358             LoginBranchname             => (C4::Context->userenv?C4::Context->userenv->{"branchname"}:"insecure"),
359             TemplateEncoding            => C4::Context->preference("TemplateEncoding"),
360             advancedMARCEditor          => C4::Context->preference("advancedMARCEditor"),
361             canreservefromotherbranches => C4::Context->preference('canreservefromotherbranches'),
362             intranetcolorstylesheet     => C4::Context->preference("intranetcolorstylesheet"),
363             intranetreadinghistory      => C4::Context->preference("intranetreadinghistory"),
364             intranetstylesheet          => C4::Context->preference("intranetstylesheet"),
365             intranetuserjs              => C4::Context->preference("intranetuserjs"),
366             intranetbookbag             => C4::Context->preference("intranetbookbag"),
367             noItemTypeImages            => C4::Context->preference("noItemTypeImages"),
368             suggestion                  => C4::Context->preference("suggestion"),
369             virtualshelves              => C4::Context->preference("virtualshelves"),
370             StaffSerialIssueDisplayCount => C4::Context->preference("StaffSerialIssueDisplayCount"),
371             NoZebra                     => C4::Context->preference('NoZebra'),
372         );
373     }
374     else {
375         warn "template type should be OPAC, here it is=[" . $in->{'type'} . "]" unless ( $in->{'type'} eq 'opac' );
376         #TODO : replace LibraryName syspref with 'system name', and remove this html processing
377         my $LibraryNameTitle = C4::Context->preference("LibraryName");
378         $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
379         $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
380         # variables passed from CGI: opac_css_override and opac_search_limits.
381         my $opac_search_limit = $ENV{'OPAC_SEARCH_LIMIT'};
382         my $opac_limit_override = $ENV{'OPAC_LIMIT_OVERRIDE'};
383         my $mylibraryfirst = C4::Context->preference("SearchMyLibraryFirst");
384         my $opac_name;
385         if($opac_limit_override && ($opac_search_limit =~ /branch:(\w+)/) ){
386              $opac_name = C4::Branch::GetBranchName($1)   # opac_search_limit is a branch, so we use it.
387         } elsif($mylibraryfirst){
388             $opac_name = C4::Branch::GetBranchName($mylibraryfirst);
389         }
390         $template->param(
391             AmazonContent             => "" . C4::Context->preference("AmazonContent"),
392             AnonSuggestions           => "" . C4::Context->preference("AnonSuggestions"),
393             AuthorisedValueImages     => C4::Context->preference("AuthorisedValueImages"),
394             LibraryName               => "" . C4::Context->preference("LibraryName"),
395             LibraryNameTitle          => "" . $LibraryNameTitle,
396             LoginBranchname           => C4::Context->userenv?C4::Context->userenv->{"branchname"}:"",
397             OPACAmazonEnabled         => C4::Context->preference("OPACAmazonEnabled"),
398             OPACAmazonSimilarItems    => C4::Context->preference("OPACAmazonSimilarItems"),
399             OPACAmazonCoverImages     => C4::Context->preference("OPACAmazonCoverImages"),
400             OPACAmazonReviews         => C4::Context->preference("OPACAmazonReviews"),
401             OPACFRBRizeEditions       => C4::Context->preference("OPACFRBRizeEditions"),
402             OpacHighlightedWords       => C4::Context->preference("OpacHighlightedWords"),
403             OPACItemHolds             => C4::Context->preference("OPACItemHolds"),
404             OPACShelfBrowser          => "". C4::Context->preference("OPACShelfBrowser"),
405             OPACURLOpenInNewWindow    => "" . C4::Context->preference("OPACURLOpenInNewWindow"),
406             OPACUserCSS               => "". C4::Context->preference("OPACUserCSS"),
407             OPACViewOthersSuggestions => "" . C4::Context->preference("OPACViewOthersSuggestions"),
408             OpacAuthorities           => C4::Context->preference("OpacAuthorities"),
409             OPACBaseURL               => ($in->{'query'}->https() ? "https://" : "http://") . $ENV{'SERVER_NAME'} .
410                    ($ENV{'SERVER_PORT'} eq ($in->{'query'}->https() ? "443" : "80") ? '' : ":$ENV{'SERVER_PORT'}"),
411             opac_name             => $opac_name,
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             OpacMainUserBlock         => "" . C4::Context->preference("OpacMainUserBlock"),
418             OpacNav                   => "" . C4::Context->preference("OpacNav"),
419             OpacPasswordChange        => C4::Context->preference("OpacPasswordChange"),
420             OPACPatronDetails        => C4::Context->preference("OPACPatronDetails"),
421             OPACFinesTab              => C4::Context->preference("OPACFinesTab"),
422             OpacTopissue              => C4::Context->preference("OpacTopissue"),
423             RequestOnOpac             => C4::Context->preference("RequestOnOpac"),
424             TemplateEncoding          => "". C4::Context->preference("TemplateEncoding"),
425             'Version'                 => C4::Context->preference('Version'),
426             hidelostitems             => C4::Context->preference("hidelostitems"),
427             mylibraryfirst            => (C4::Context->preference("SearchMyLibraryFirst") && C4::Context->userenv) ? C4::Context->userenv->{'branch'} : '',
428             opaclayoutstylesheet      => "" . C4::Context->preference("opaclayoutstylesheet"),
429             opaccolorstylesheet       => "" . C4::Context->preference("opaccolorstylesheet"),
430             opacstylesheet            => "" . C4::Context->preference("opacstylesheet"),
431             opacbookbag               => "" . C4::Context->preference("opacbookbag"),
432             opaccredits               => "" . C4::Context->preference("opaccredits"),
433             opacheader                => "" . C4::Context->preference("opacheader"),
434             opaclanguagesdisplay      => "" . C4::Context->preference("opaclanguagesdisplay"),
435             opacreadinghistory        => C4::Context->preference("opacreadinghistory"),
436             opacsmallimage            => "" . C4::Context->preference("opacsmallimage"),
437             opacuserjs                => C4::Context->preference("opacuserjs"),
438             opacuserlogin             => "" . C4::Context->preference("opacuserlogin"),
439             reviewson                 => C4::Context->preference("reviewson"),
440             suggestion                => "" . C4::Context->preference("suggestion"),
441             virtualshelves            => "" . C4::Context->preference("virtualshelves"),
442             OPACSerialIssueDisplayCount => C4::Context->preference("OPACSerialIssueDisplayCount"),
443             OpacAddMastheadLibraryPulldown => C4::Context->preference("OpacAddMastheadLibraryPulldown"),
444             OPACXSLTDetailsDisplay           => C4::Context->preference("OPACXSLTDetailsDisplay"),
445             OPACXSLTResultsDisplay           => C4::Context->preference("OPACXSLTResultsDisplay"),
446             SyndeticsClientCode          => C4::Context->preference("SyndeticsClientCode"),
447             SyndeticsEnabled             => C4::Context->preference("SyndeticsEnabled"),
448             SyndeticsCoverImages         => C4::Context->preference("SyndeticsCoverImages"),
449             SyndeticsTOC                 => C4::Context->preference("SyndeticsTOC"),
450             SyndeticsSummary             => C4::Context->preference("SyndeticsSummary"),
451             SyndeticsEditions            => C4::Context->preference("SyndeticsEditions"),
452             SyndeticsExcerpt             => C4::Context->preference("SyndeticsExcerpt"),
453             SyndeticsReviews             => C4::Context->preference("SyndeticsReviews"),
454             SyndeticsAuthorNotes         => C4::Context->preference("SyndeticsAuthorNotes"),
455             SyndeticsAwards              => C4::Context->preference("SyndeticsAwards"),
456             SyndeticsSeries              => C4::Context->preference("SyndeticsSeries"),
457             SyndeticsCoverImageSize      => C4::Context->preference("SyndeticsCoverImageSize"),
458         );
459     }
460         $template->param(listloop=>[{shelfname=>"Freelist", shelfnumber=>110}]);
461     return ( $template, $borrowernumber, $cookie, $flags);
462 }
463
464 =head2 checkauth
465
466   ($userid, $cookie, $sessionID) = &checkauth($query, $noauth, $flagsrequired, $type);
467
468 Verifies that the user is authorized to run this script.  If
469 the user is authorized, a (userid, cookie, session-id, flags)
470 quadruple is returned.  If the user is not authorized but does
471 not have the required privilege (see $flagsrequired below), it
472 displays an error page and exits.  Otherwise, it displays the
473 login page and exits.
474
475 Note that C<&checkauth> will return if and only if the user
476 is authorized, so it should be called early on, before any
477 unfinished operations (e.g., if you've opened a file, then
478 C<&checkauth> won't close it for you).
479
480 C<$query> is the CGI object for the script calling C<&checkauth>.
481
482 The C<$noauth> argument is optional. If it is set, then no
483 authorization is required for the script.
484
485 C<&checkauth> fetches user and session information from C<$query> and
486 ensures that the user is authorized to run scripts that require
487 authorization.
488
489 The C<$flagsrequired> argument specifies the required privileges
490 the user must have if the username and password are correct.
491 It should be specified as a reference-to-hash; keys in the hash
492 should be the "flags" for the user, as specified in the Members
493 intranet module. Any key specified must correspond to a "flag"
494 in the userflags table. E.g., { circulate => 1 } would specify
495 that the user must have the "circulate" privilege in order to
496 proceed. To make sure that access control is correct, the
497 C<$flagsrequired> parameter must be specified correctly.
498
499 Koha also has a concept of sub-permissions, also known as
500 granular permissions.  This makes the value of each key
501 in the C<flagsrequired> hash take on an additional
502 meaning, i.e.,
503
504  1
505
506 The user must have access to all subfunctions of the module
507 specified by the hash key.
508
509  *
510
511 The user must have access to at least one subfunction of the module
512 specified by the hash key.
513
514  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 =head2 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
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 =head2 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
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 =head2 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 =head2 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 =head2 get_user_subpermissions
1462
1463   $user_perm_hashref = get_user_subpermissions($userid);
1464
1465 Given the userid (note, not the borrowernumber) of a staff user,
1466 return a hashref of hashrefs of the specific subpermissions
1467 accorded to the user.  An example return is
1468
1469  {
1470     tools => {
1471         export_catalog => 1,
1472         import_patrons => 1,
1473     }
1474  }
1475
1476 The top-level hash-key is a module or function code from
1477 userflags.flag, while the second-level key is a code
1478 from permissions.
1479
1480 The results of this function do not give a complete picture
1481 of the functions that a staff user can access; it is also
1482 necessary to check borrowers.flags.
1483
1484 =cut
1485
1486 sub get_user_subpermissions {
1487     my $userid = shift;
1488
1489     my $dbh = C4::Context->dbh;
1490     my $sth = $dbh->prepare("SELECT flag, user_permissions.code
1491                              FROM user_permissions
1492                              JOIN permissions USING (module_bit, code)
1493                              JOIN userflags ON (module_bit = bit)
1494                              JOIN borrowers USING (borrowernumber)
1495                              WHERE userid = ?");
1496     $sth->execute($userid);
1497
1498     my $user_perms = {};
1499     while (my $perm = $sth->fetchrow_hashref) {
1500         $user_perms->{$perm->{'flag'}}->{$perm->{'code'}} = 1;
1501     }
1502     return $user_perms;
1503 }
1504
1505 =head2 get_all_subpermissions
1506
1507   my $perm_hashref = get_all_subpermissions();
1508
1509 Returns a hashref of hashrefs defining all specific
1510 permissions currently defined.  The return value
1511 has the same structure as that of C<get_user_subpermissions>,
1512 except that the innermost hash value is the description
1513 of the subpermission.
1514
1515 =cut
1516
1517 sub get_all_subpermissions {
1518     my $dbh = C4::Context->dbh;
1519     my $sth = $dbh->prepare("SELECT flag, code, description
1520                              FROM permissions
1521                              JOIN userflags ON (module_bit = bit)");
1522     $sth->execute();
1523
1524     my $all_perms = {};
1525     while (my $perm = $sth->fetchrow_hashref) {
1526         $all_perms->{$perm->{'flag'}}->{$perm->{'code'}} = $perm->{'description'};
1527     }
1528     return $all_perms;
1529 }
1530
1531 =head2 haspermission
1532
1533   $flags = ($userid, $flagsrequired);
1534
1535 C<$userid> the userid of the member
1536 C<$flags> is a hashref of required flags like C<$borrower-&lt;{authflags}> 
1537
1538 Returns member's flags or 0 if a permission is not met.
1539
1540 =cut
1541
1542 sub haspermission {
1543     my ($userid, $flagsrequired) = @_;
1544     my $sth = C4::Context->dbh->prepare("SELECT flags FROM borrowers WHERE userid=?");
1545     $sth->execute($userid);
1546     my $flags = getuserflags( $sth->fetchrow(), $userid );
1547     if ( $userid eq C4::Context->config('user') ) {
1548         # Super User Account from /etc/koha.conf
1549         $flags->{'superlibrarian'} = 1;
1550     }
1551     elsif ( $userid eq 'demo' && C4::Context->config('demo') ) {
1552         # Demo user that can do "anything" (demo=1 in /etc/koha.conf)
1553         $flags->{'superlibrarian'} = 1;
1554     }
1555     return $flags if $flags->{superlibrarian};
1556     foreach my $module ( keys %$flagsrequired ) {
1557         my $subperm = $flagsrequired->{$module};
1558         if ($subperm eq '*') {
1559             return 0 unless ( $flags->{$module} == 1 or ref($flags->{$module}) );
1560         } else {
1561             return 0 unless ( $flags->{$module} == 1 or
1562                                 ( ref($flags->{$module}) and
1563                                   exists $flags->{$module}->{$subperm} and
1564                                   $flags->{$module}->{$subperm} == 1
1565                                 )
1566                             );
1567         }
1568     }
1569     return $flags;
1570     #FIXME - This fcn should return the failed permission so a suitable error msg can be delivered.
1571 }
1572
1573
1574 sub getborrowernumber {
1575     my ($userid) = @_;
1576     my $userenv = C4::Context->userenv;
1577     if ( defined( $userenv ) && ref( $userenv ) eq 'HASH' && $userenv->{number} ) {
1578         return $userenv->{number};
1579     }
1580     my $dbh = C4::Context->dbh;
1581     for my $field ( 'userid', 'cardnumber' ) {
1582         my $sth =
1583           $dbh->prepare("select borrowernumber from borrowers where $field=?");
1584         $sth->execute($userid);
1585         if ( $sth->rows ) {
1586             my ($bnumber) = $sth->fetchrow;
1587             return $bnumber;
1588         }
1589     }
1590     return 0;
1591 }
1592
1593 END { }    # module clean-up code here (global destructor)
1594 1;
1595 __END__
1596
1597 =head1 SEE ALSO
1598
1599 CGI(3)
1600
1601 C4::Output(3)
1602
1603 Digest::MD5(3)
1604
1605 =cut