Bug 5390: OPAC loses library selection
[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} eq '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             using_https                  => $in->{'query'}->https() ? 1 : 0,
341             noItemTypeImages            => C4::Context->preference("noItemTypeImages"),
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             advancedMARCEditor          => C4::Context->preference("advancedMARCEditor"),
360             canreservefromotherbranches => C4::Context->preference('canreservefromotherbranches'),
361             intranetcolorstylesheet     => C4::Context->preference("intranetcolorstylesheet"),
362             intranetreadinghistory      => C4::Context->preference("intranetreadinghistory"),
363             intranetstylesheet          => C4::Context->preference("intranetstylesheet"),
364             IntranetUserCSS             => C4::Context->preference("IntranetUserCSS"),
365             intranetuserjs              => C4::Context->preference("intranetuserjs"),
366             intranetbookbag             => C4::Context->preference("intranetbookbag"),
367             suggestion                  => C4::Context->preference("suggestion"),
368             virtualshelves              => C4::Context->preference("virtualshelves"),
369             StaffSerialIssueDisplayCount => C4::Context->preference("StaffSerialIssueDisplayCount"),
370             NoZebra                     => C4::Context->preference('NoZebra'),
371         );
372     }
373     else {
374         warn "template type should be OPAC, here it is=[" . $in->{'type'} . "]" unless ( $in->{'type'} eq 'opac' );
375         #TODO : replace LibraryName syspref with 'system name', and remove this html processing
376         my $LibraryNameTitle = C4::Context->preference("LibraryName");
377         $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
378         $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
379         # variables passed from CGI: opac_css_override and opac_search_limits.
380         my $opac_search_limit = $ENV{'OPAC_SEARCH_LIMIT'};
381         my $opac_limit_override = $ENV{'OPAC_LIMIT_OVERRIDE'};
382         my $opac_name = '';
383         if (($opac_search_limit =~ /branch:(\w+)/ && $opac_limit_override) || $in->{'query'}->param('limit') =~ /branch:(\w+)/){
384             $opac_name = $1;   # opac_search_limit is a branch, so we use it.
385         } elsif (C4::Context->preference("SearchMyLibraryFirst") && C4::Context->userenv && C4::Context->userenv->{'branch'}) {
386             $opac_name = C4::Context->userenv->{'branch'};
387         }
388         $template->param(
389             AmazonContent             => "" . C4::Context->preference("AmazonContent"),
390             AnonSuggestions           => "" . C4::Context->preference("AnonSuggestions"),
391             AuthorisedValueImages     => C4::Context->preference("AuthorisedValueImages"),
392             BranchesLoop              => GetBranchesLoop($opac_name),
393             LibraryName               => "" . C4::Context->preference("LibraryName"),
394             LibraryNameTitle          => "" . $LibraryNameTitle,
395             LoginBranchname           => C4::Context->userenv?C4::Context->userenv->{"branchname"}:"",
396             OPACAmazonEnabled         => C4::Context->preference("OPACAmazonEnabled"),
397             OPACAmazonSimilarItems    => C4::Context->preference("OPACAmazonSimilarItems"),
398             OPACAmazonCoverImages     => C4::Context->preference("OPACAmazonCoverImages"),
399             OPACAmazonReviews         => C4::Context->preference("OPACAmazonReviews"),
400             OPACFRBRizeEditions       => C4::Context->preference("OPACFRBRizeEditions"),
401             OpacHighlightedWords       => C4::Context->preference("OpacHighlightedWords"),
402             OPACItemHolds             => C4::Context->preference("OPACItemHolds"),
403             OPACShelfBrowser          => "". C4::Context->preference("OPACShelfBrowser"),
404             OPACURLOpenInNewWindow    => "" . C4::Context->preference("OPACURLOpenInNewWindow"),
405             OPACUserCSS               => "". C4::Context->preference("OPACUserCSS"),
406             OPACViewOthersSuggestions => "" . C4::Context->preference("OPACViewOthersSuggestions"),
407             OpacAuthorities           => C4::Context->preference("OpacAuthorities"),
408             OPACBaseURL               => ($in->{'query'}->https() ? "https://" : "http://") . $ENV{'SERVER_NAME'} .
409                    ($ENV{'SERVER_PORT'} eq ($in->{'query'}->https() ? "443" : "80") ? '' : ":$ENV{'SERVER_PORT'}"),
410             opac_css_override           => $ENV{'OPAC_CSS_OVERRIDE'},
411             opac_search_limit         => $opac_search_limit,
412             opac_limit_override       => $opac_limit_override,
413             OpacBrowser               => C4::Context->preference("OpacBrowser"),
414             OpacCloud                 => C4::Context->preference("OpacCloud"),
415             OpacMainUserBlock         => "" . C4::Context->preference("OpacMainUserBlock"),
416             OpacNav                   => "" . C4::Context->preference("OpacNav"),
417             OpacPasswordChange        => C4::Context->preference("OpacPasswordChange"),
418             OPACPatronDetails        => C4::Context->preference("OPACPatronDetails"),
419             OPACPrivacy               => C4::Context->preference("OPACPrivacy"),
420             OPACFinesTab              => C4::Context->preference("OPACFinesTab"),
421             OpacTopissue              => C4::Context->preference("OpacTopissue"),
422             RequestOnOpac             => C4::Context->preference("RequestOnOpac"),
423             'Version'                 => C4::Context->preference('Version'),
424             hidelostitems             => C4::Context->preference("hidelostitems"),
425             mylibraryfirst            => (C4::Context->preference("SearchMyLibraryFirst") && C4::Context->userenv) ? C4::Context->userenv->{'branch'} : '',
426             opaclayoutstylesheet      => "" . C4::Context->preference("opaclayoutstylesheet"),
427             opaccolorstylesheet       => "" . C4::Context->preference("opaccolorstylesheet"),
428             opacstylesheet            => "" . C4::Context->preference("opacstylesheet"),
429             opacbookbag               => "" . C4::Context->preference("opacbookbag"),
430             opaccredits               => "" . C4::Context->preference("opaccredits"),
431             opacheader                => "" . C4::Context->preference("opacheader"),
432             opaclanguagesdisplay      => "" . C4::Context->preference("opaclanguagesdisplay"),
433             opacreadinghistory        => C4::Context->preference("opacreadinghistory"),
434             opacsmallimage            => "" . C4::Context->preference("opacsmallimage"),
435             opacuserjs                => C4::Context->preference("opacuserjs"),
436             opacuserlogin             => "" . C4::Context->preference("opacuserlogin"),
437             reviewson                 => C4::Context->preference("reviewson"),
438             suggestion                => "" . C4::Context->preference("suggestion"),
439             virtualshelves            => "" . C4::Context->preference("virtualshelves"),
440             OPACSerialIssueDisplayCount => C4::Context->preference("OPACSerialIssueDisplayCount"),
441             OpacAddMastheadLibraryPulldown => C4::Context->preference("OpacAddMastheadLibraryPulldown"),
442             OPACXSLTDetailsDisplay           => C4::Context->preference("OPACXSLTDetailsDisplay"),
443             OPACXSLTResultsDisplay           => C4::Context->preference("OPACXSLTResultsDisplay"),
444             SyndeticsClientCode          => C4::Context->preference("SyndeticsClientCode"),
445             SyndeticsEnabled             => C4::Context->preference("SyndeticsEnabled"),
446             SyndeticsCoverImages         => C4::Context->preference("SyndeticsCoverImages"),
447             SyndeticsTOC                 => C4::Context->preference("SyndeticsTOC"),
448             SyndeticsSummary             => C4::Context->preference("SyndeticsSummary"),
449             SyndeticsEditions            => C4::Context->preference("SyndeticsEditions"),
450             SyndeticsExcerpt             => C4::Context->preference("SyndeticsExcerpt"),
451             SyndeticsReviews             => C4::Context->preference("SyndeticsReviews"),
452             SyndeticsAuthorNotes         => C4::Context->preference("SyndeticsAuthorNotes"),
453             SyndeticsAwards              => C4::Context->preference("SyndeticsAwards"),
454             SyndeticsSeries              => C4::Context->preference("SyndeticsSeries"),
455             SyndeticsCoverImageSize      => C4::Context->preference("SyndeticsCoverImageSize"),
456         );
457
458         $template->param(OpacPublic => '1') if ($template->param( 'loggedinusername') || C4::Context->preference("OpacPublic"));
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'},(strftime '%c', 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         IndependantBranches=> C4::Context->preference("IndependantBranches"),
944         AutoLocation       => C4::Context->preference("AutoLocation"),
945                 wrongip            => $info{'wrongip'},
946     );
947
948     $template->param( OpacPublic => C4::Context->preference("OpacPublic"));
949     $template->param( loginprompt => 1 ) unless $info{'nopermission'};
950
951     if ($cas) { 
952         $template->param(
953             casServerUrl    => login_cas_url(),
954             invalidCasLogin => $info{'invalidCasLogin'}
955         );
956     }
957
958     my $self_url = $query->url( -absolute => 1 );
959     $template->param(
960         url         => $self_url,
961         LibraryName => C4::Context->preference("LibraryName"),
962     );
963     $template->param( \%info );
964 #    $cookie = $query->cookie(CGISESSID => $session->id
965 #   );
966     print $query->header(
967         -type   => 'text/html',
968         -charset => 'utf-8',
969         -cookie => $cookie
970       ),
971       $template->output;
972     exit;
973 }
974
975 =head2 check_api_auth
976
977   ($status, $cookie, $sessionId) = check_api_auth($query, $userflags);
978
979 Given a CGI query containing the parameters 'userid' and 'password' and/or a session
980 cookie, determine if the user has the privileges specified by C<$userflags>.
981
982 C<check_api_auth> is is meant for authenticating users of web services, and
983 consequently will always return and will not attempt to redirect the user
984 agent.
985
986 If a valid session cookie is already present, check_api_auth will return a status
987 of "ok", the cookie, and the Koha session ID.
988
989 If no session cookie is present, check_api_auth will check the 'userid' and 'password
990 parameters and create a session cookie and Koha session if the supplied credentials
991 are OK.
992
993 Possible return values in C<$status> are:
994
995 =over
996
997 =item "ok" -- user authenticated; C<$cookie> and C<$sessionid> have valid values.
998
999 =item "failed" -- credentials are not correct; C<$cookie> and C<$sessionid> are undef
1000
1001 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1002
1003 =item "expired -- session cookie has expired; API user should resubmit userid and password
1004
1005 =back
1006
1007 =cut
1008
1009 sub check_api_auth {
1010     my $query = shift;
1011     my $flagsrequired = shift;
1012
1013     my $dbh     = C4::Context->dbh;
1014     my $timeout = C4::Context->preference('timeout');
1015     $timeout = 600 unless $timeout;
1016
1017     unless (C4::Context->preference('Version')) {
1018         # database has not been installed yet
1019         return ("maintenance", undef, undef);
1020     }
1021     my $kohaversion=C4::Context::KOHAVERSION;
1022     $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1023     if (C4::Context->preference('Version') < $kohaversion) {
1024         # database in need of version update; assume that
1025         # no API should be called while databsae is in
1026         # this condition.
1027         return ("maintenance", undef, undef);
1028     }
1029
1030     # FIXME -- most of what follows is a copy-and-paste
1031     # of code from checkauth.  There is an obvious need
1032     # for refactoring to separate the various parts of
1033     # the authentication code, but as of 2007-11-19 this
1034     # is deferred so as to not introduce bugs into the
1035     # regular authentication code for Koha 3.0.
1036
1037     # see if we have a valid session cookie already
1038     # however, if a userid parameter is present (i.e., from
1039     # a form submission, assume that any current cookie
1040     # is to be ignored
1041     my $sessionID = undef;
1042     unless ($query->param('userid')) {
1043         $sessionID = $query->cookie("CGISESSID");
1044     }
1045     if ($sessionID) {
1046         my $session = get_session($sessionID);
1047         C4::Context->_new_userenv($sessionID);
1048         if ($session) {
1049             C4::Context::set_userenv(
1050                 $session->param('number'),       $session->param('id'),
1051                 $session->param('cardnumber'),   $session->param('firstname'),
1052                 $session->param('surname'),      $session->param('branch'),
1053                 $session->param('branchname'),   $session->param('flags'),
1054                 $session->param('emailaddress'), $session->param('branchprinter')
1055             );
1056
1057             my $ip = $session->param('ip');
1058             my $lasttime = $session->param('lasttime');
1059             my $userid = $session->param('id');
1060             if ( $lasttime < time() - $timeout ) {
1061                 # time out
1062                 $session->delete();
1063                 C4::Context->_unset_userenv($sessionID);
1064                 $userid    = undef;
1065                 $sessionID = undef;
1066                 return ("expired", undef, undef);
1067             } elsif ( $ip ne $ENV{'REMOTE_ADDR'} ) {
1068                 # IP address changed
1069                 $session->delete();
1070                 C4::Context->_unset_userenv($sessionID);
1071                 $userid    = undef;
1072                 $sessionID = undef;
1073                 return ("expired", undef, undef);
1074             } else {
1075                 my $cookie = $query->cookie( CGISESSID => $session->id );
1076                 $session->param('lasttime',time());
1077                 my $flags = haspermission($userid, $flagsrequired);
1078                 if ($flags) {
1079                     return ("ok", $cookie, $sessionID);
1080                 } else {
1081                     $session->delete();
1082                     C4::Context->_unset_userenv($sessionID);
1083                     $userid    = undef;
1084                     $sessionID = undef;
1085                     return ("failed", undef, undef);
1086                 }
1087             }
1088         } else {
1089             return ("expired", undef, undef);
1090         }
1091     } else {
1092         # new login
1093         my $userid = $query->param('userid');
1094         my $password = $query->param('password');
1095         unless ($userid and $password) {
1096             # caller did something wrong, fail the authenticateion
1097             return ("failed", undef, undef);
1098         }
1099         my ($return, $cardnumber);
1100         if ($cas && $query->param('ticket')) {
1101             my $retuserid;
1102             ( $return, $cardnumber, $retuserid ) = checkpw( $dbh, $userid, $password, $query );
1103             $userid = $retuserid;
1104         } else {
1105             ( $return, $cardnumber ) = checkpw( $dbh, $userid, $password, $query );
1106         }
1107         if ($return and haspermission(  $userid, $flagsrequired)) {
1108             my $session = get_session("");
1109             return ("failed", undef, undef) unless $session;
1110
1111             my $sessionID = $session->id;
1112             C4::Context->_new_userenv($sessionID);
1113             my $cookie = $query->cookie(CGISESSID => $sessionID);
1114             if ( $return == 1 ) {
1115                 my (
1116                     $borrowernumber, $firstname,  $surname,
1117                     $userflags,      $branchcode, $branchname,
1118                     $branchprinter,  $emailaddress
1119                 );
1120                 my $sth =
1121                   $dbh->prepare(
1122 "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=?"
1123                   );
1124                 $sth->execute($userid);
1125                 (
1126                     $borrowernumber, $firstname,  $surname,
1127                     $userflags,      $branchcode, $branchname,
1128                     $branchprinter,  $emailaddress
1129                 ) = $sth->fetchrow if ( $sth->rows );
1130
1131                 unless ($sth->rows ) {
1132                     my $sth = $dbh->prepare(
1133 "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=?"
1134                       );
1135                     $sth->execute($cardnumber);
1136                     (
1137                         $borrowernumber, $firstname,  $surname,
1138                         $userflags,      $branchcode, $branchname,
1139                         $branchprinter,  $emailaddress
1140                     ) = $sth->fetchrow if ( $sth->rows );
1141
1142                     unless ( $sth->rows ) {
1143                         $sth->execute($userid);
1144                         (
1145                             $borrowernumber, $firstname, $surname, $userflags,
1146                             $branchcode, $branchname, $branchprinter, $emailaddress
1147                         ) = $sth->fetchrow if ( $sth->rows );
1148                     }
1149                 }
1150
1151                 my $ip       = $ENV{'REMOTE_ADDR'};
1152                 # if they specify at login, use that
1153                 if ($query->param('branch')) {
1154                     $branchcode  = $query->param('branch');
1155                     $branchname = GetBranchName($branchcode);
1156                 }
1157                 my $branches = GetBranches();
1158                 my @branchesloop;
1159                 foreach my $br ( keys %$branches ) {
1160                     #     now we work with the treatment of ip
1161                     my $domain = $branches->{$br}->{'branchip'};
1162                     if ( $domain && $ip =~ /^$domain/ ) {
1163                         $branchcode = $branches->{$br}->{'branchcode'};
1164
1165                         # new op dev : add the branchprinter and branchname in the cookie
1166                         $branchprinter = $branches->{$br}->{'branchprinter'};
1167                         $branchname    = $branches->{$br}->{'branchname'};
1168                     }
1169                 }
1170                 $session->param('number',$borrowernumber);
1171                 $session->param('id',$userid);
1172                 $session->param('cardnumber',$cardnumber);
1173                 $session->param('firstname',$firstname);
1174                 $session->param('surname',$surname);
1175                 $session->param('branch',$branchcode);
1176                 $session->param('branchname',$branchname);
1177                 $session->param('flags',$userflags);
1178                 $session->param('emailaddress',$emailaddress);
1179                 $session->param('ip',$session->remote_addr());
1180                 $session->param('lasttime',time());
1181             } elsif ( $return == 2 ) {
1182                 #We suppose the user is the superlibrarian
1183                 $session->param('number',0);
1184                 $session->param('id',C4::Context->config('user'));
1185                 $session->param('cardnumber',C4::Context->config('user'));
1186                 $session->param('firstname',C4::Context->config('user'));
1187                 $session->param('surname',C4::Context->config('user'));
1188                 $session->param('branch','NO_LIBRARY_SET');
1189                 $session->param('branchname','NO_LIBRARY_SET');
1190                 $session->param('flags',1);
1191                 $session->param('emailaddress', C4::Context->preference('KohaAdminEmailAddress'));
1192                 $session->param('ip',$session->remote_addr());
1193                 $session->param('lasttime',time());
1194             }
1195             C4::Context::set_userenv(
1196                 $session->param('number'),       $session->param('id'),
1197                 $session->param('cardnumber'),   $session->param('firstname'),
1198                 $session->param('surname'),      $session->param('branch'),
1199                 $session->param('branchname'),   $session->param('flags'),
1200                 $session->param('emailaddress'), $session->param('branchprinter')
1201             );
1202             return ("ok", $cookie, $sessionID);
1203         } else {
1204             return ("failed", undef, undef);
1205         }
1206     }
1207 }
1208
1209 =head2 check_cookie_auth
1210
1211   ($status, $sessionId) = check_api_auth($cookie, $userflags);
1212
1213 Given a CGISESSID cookie set during a previous login to Koha, determine
1214 if the user has the privileges specified by C<$userflags>.
1215
1216 C<check_cookie_auth> is meant for authenticating special services
1217 such as tools/upload-file.pl that are invoked by other pages that
1218 have been authenticated in the usual way.
1219
1220 Possible return values in C<$status> are:
1221
1222 =over
1223
1224 =item "ok" -- user authenticated; C<$sessionID> have valid values.
1225
1226 =item "failed" -- credentials are not correct; C<$sessionid> are undef
1227
1228 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
1229
1230 =item "expired -- session cookie has expired; API user should resubmit userid and password
1231
1232 =back
1233
1234 =cut
1235
1236 sub check_cookie_auth {
1237     my $cookie = shift;
1238     my $flagsrequired = shift;
1239
1240     my $dbh     = C4::Context->dbh;
1241     my $timeout = C4::Context->preference('timeout');
1242     $timeout = 600 unless $timeout;
1243
1244     unless (C4::Context->preference('Version')) {
1245         # database has not been installed yet
1246         return ("maintenance", undef);
1247     }
1248     my $kohaversion=C4::Context::KOHAVERSION;
1249     $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
1250     if (C4::Context->preference('Version') < $kohaversion) {
1251         # database in need of version update; assume that
1252         # no API should be called while databsae is in
1253         # this condition.
1254         return ("maintenance", undef);
1255     }
1256
1257     # FIXME -- most of what follows is a copy-and-paste
1258     # of code from checkauth.  There is an obvious need
1259     # for refactoring to separate the various parts of
1260     # the authentication code, but as of 2007-11-23 this
1261     # is deferred so as to not introduce bugs into the
1262     # regular authentication code for Koha 3.0.
1263
1264     # see if we have a valid session cookie already
1265     # however, if a userid parameter is present (i.e., from
1266     # a form submission, assume that any current cookie
1267     # is to be ignored
1268     unless (defined $cookie and $cookie) {
1269         return ("failed", undef);
1270     }
1271     my $sessionID = $cookie;
1272     my $session = get_session($sessionID);
1273     C4::Context->_new_userenv($sessionID);
1274     if ($session) {
1275         C4::Context::set_userenv(
1276             $session->param('number'),       $session->param('id'),
1277             $session->param('cardnumber'),   $session->param('firstname'),
1278             $session->param('surname'),      $session->param('branch'),
1279             $session->param('branchname'),   $session->param('flags'),
1280             $session->param('emailaddress'), $session->param('branchprinter')
1281         );
1282
1283         my $ip = $session->param('ip');
1284         my $lasttime = $session->param('lasttime');
1285         my $userid = $session->param('id');
1286         if ( $lasttime < time() - $timeout ) {
1287             # time out
1288             $session->delete();
1289             C4::Context->_unset_userenv($sessionID);
1290             $userid    = undef;
1291             $sessionID = undef;
1292             return ("expired", undef);
1293         } elsif ( $ip ne $ENV{'REMOTE_ADDR'} ) {
1294             # IP address changed
1295             $session->delete();
1296             C4::Context->_unset_userenv($sessionID);
1297             $userid    = undef;
1298             $sessionID = undef;
1299             return ("expired", undef);
1300         } else {
1301             $session->param('lasttime',time());
1302             my $flags = haspermission($userid, $flagsrequired);
1303             if ($flags) {
1304                 return ("ok", $sessionID);
1305             } else {
1306                 $session->delete();
1307                 C4::Context->_unset_userenv($sessionID);
1308                 $userid    = undef;
1309                 $sessionID = undef;
1310                 return ("failed", undef);
1311             }
1312         }
1313     } else {
1314         return ("expired", undef);
1315     }
1316 }
1317
1318 =head2 get_session
1319
1320   use CGI::Session;
1321   my $session = get_session($sessionID);
1322
1323 Given a session ID, retrieve the CGI::Session object used to store
1324 the session's state.  The session object can be used to store
1325 data that needs to be accessed by different scripts during a
1326 user's session.
1327
1328 If the C<$sessionID> parameter is an empty string, a new session
1329 will be created.
1330
1331 =cut
1332
1333 sub get_session {
1334     my $sessionID = shift;
1335     my $storage_method = C4::Context->preference('SessionStorage');
1336     my $dbh = C4::Context->dbh;
1337     my $session;
1338     if ($storage_method eq 'mysql'){
1339         $session = new CGI::Session("driver:MySQL;serializer:yaml;id:md5", $sessionID, {Handle=>$dbh});
1340     }
1341     elsif ($storage_method eq 'Pg') {
1342         $session = new CGI::Session("driver:PostgreSQL;serializer:yaml;id:md5", $sessionID, {Handle=>$dbh});
1343     }
1344     else {
1345         # catch all defaults to tmp should work on all systems
1346         $session = new CGI::Session("driver:File;serializer:yaml;id:md5", $sessionID, {Directory=>'/tmp'});
1347     }
1348     return $session;
1349 }
1350
1351 sub checkpw {
1352
1353     my ( $dbh, $userid, $password, $query ) = @_;
1354     if ($ldap) {
1355         $debug and print "## checkpw - checking LDAP\n";
1356         my ($retval,$retcard) = checkpw_ldap(@_);    # EXTERNAL AUTH
1357         ($retval) and return ($retval,$retcard);
1358     }
1359
1360     if ($cas && $query->param('ticket')) {
1361         $debug and print STDERR "## checkpw - checking CAS\n";
1362         # In case of a CAS authentication, we use the ticket instead of the password
1363         my $ticket = $query->param('ticket');
1364         my ($retval,$retcard,$retuserid) = checkpw_cas($dbh, $ticket, $query);    # EXTERNAL AUTH
1365         ($retval) and return ($retval,$retcard,$retuserid);
1366         return 0;
1367     }
1368
1369     # INTERNAL AUTH
1370     my $sth =
1371       $dbh->prepare(
1372 "select password,cardnumber,borrowernumber,userid,firstname,surname,branchcode,flags from borrowers where userid=?"
1373       );
1374     $sth->execute($userid);
1375     if ( $sth->rows ) {
1376         my ( $md5password, $cardnumber, $borrowernumber, $userid, $firstname,
1377             $surname, $branchcode, $flags )
1378           = $sth->fetchrow;
1379         if ( md5_base64($password) eq $md5password and $md5password ne "!") {
1380
1381             C4::Context->set_userenv( "$borrowernumber", $userid, $cardnumber,
1382                 $firstname, $surname, $branchcode, $flags );
1383             return 1, $cardnumber;
1384         }
1385     }
1386     $sth =
1387       $dbh->prepare(
1388 "select password,cardnumber,borrowernumber,userid, firstname,surname,branchcode,flags from borrowers where cardnumber=?"
1389       );
1390     $sth->execute($userid);
1391     if ( $sth->rows ) {
1392         my ( $md5password, $cardnumber, $borrowernumber, $userid, $firstname,
1393             $surname, $branchcode, $flags )
1394           = $sth->fetchrow;
1395         if ( md5_base64($password) eq $md5password ) {
1396
1397             C4::Context->set_userenv( $borrowernumber, $userid, $cardnumber,
1398                 $firstname, $surname, $branchcode, $flags );
1399             return 1, $userid;
1400         }
1401     }
1402     if (   $userid && $userid eq C4::Context->config('user')
1403         && "$password" eq C4::Context->config('pass') )
1404     {
1405
1406 # Koha superuser account
1407 #     C4::Context->set_userenv(0,0,C4::Context->config('user'),C4::Context->config('user'),C4::Context->config('user'),"",1);
1408         return 2;
1409     }
1410     if (   $userid && $userid eq 'demo'
1411         && "$password" eq 'demo'
1412         && C4::Context->config('demo') )
1413     {
1414
1415 # DEMO => the demo user is allowed to do everything (if demo set to 1 in koha.conf
1416 # some features won't be effective : modify systempref, modify MARC structure,
1417         return 2;
1418     }
1419     return 0;
1420 }
1421
1422 =head2 getuserflags
1423
1424     my $authflags = getuserflags($flags, $userid, [$dbh]);
1425
1426 Translates integer flags into permissions strings hash.
1427
1428 C<$flags> is the integer userflags value ( borrowers.userflags )
1429 C<$userid> is the members.userid, used for building subpermissions
1430 C<$authflags> is a hashref of permissions
1431
1432 =cut
1433
1434 sub getuserflags {
1435     my $flags   = shift;
1436     my $userid  = shift;
1437     my $dbh     = @_ ? shift : C4::Context->dbh;
1438     my $userflags;
1439     $flags = 0 unless $flags;
1440     my $sth = $dbh->prepare("SELECT bit, flag, defaulton FROM userflags");
1441     $sth->execute;
1442
1443     while ( my ( $bit, $flag, $defaulton ) = $sth->fetchrow ) {
1444         if ( ( $flags & ( 2**$bit ) ) || $defaulton ) {
1445             $userflags->{$flag} = 1;
1446         }
1447         else {
1448             $userflags->{$flag} = 0;
1449         }
1450     }
1451
1452     # get subpermissions and merge with top-level permissions
1453     my $user_subperms = get_user_subpermissions($userid);
1454     foreach my $module (keys %$user_subperms) {
1455         next if $userflags->{$module} == 1; # user already has permission for everything in this module
1456         $userflags->{$module} = $user_subperms->{$module};
1457     }
1458
1459     return $userflags;
1460 }
1461
1462 =head2 get_user_subpermissions
1463
1464   $user_perm_hashref = get_user_subpermissions($userid);
1465
1466 Given the userid (note, not the borrowernumber) of a staff user,
1467 return a hashref of hashrefs of the specific subpermissions
1468 accorded to the user.  An example return is
1469
1470  {
1471     tools => {
1472         export_catalog => 1,
1473         import_patrons => 1,
1474     }
1475  }
1476
1477 The top-level hash-key is a module or function code from
1478 userflags.flag, while the second-level key is a code
1479 from permissions.
1480
1481 The results of this function do not give a complete picture
1482 of the functions that a staff user can access; it is also
1483 necessary to check borrowers.flags.
1484
1485 =cut
1486
1487 sub get_user_subpermissions {
1488     my $userid = shift;
1489
1490     my $dbh = C4::Context->dbh;
1491     my $sth = $dbh->prepare("SELECT flag, user_permissions.code
1492                              FROM user_permissions
1493                              JOIN permissions USING (module_bit, code)
1494                              JOIN userflags ON (module_bit = bit)
1495                              JOIN borrowers USING (borrowernumber)
1496                              WHERE userid = ?");
1497     $sth->execute($userid);
1498
1499     my $user_perms = {};
1500     while (my $perm = $sth->fetchrow_hashref) {
1501         $user_perms->{$perm->{'flag'}}->{$perm->{'code'}} = 1;
1502     }
1503     return $user_perms;
1504 }
1505
1506 =head2 get_all_subpermissions
1507
1508   my $perm_hashref = get_all_subpermissions();
1509
1510 Returns a hashref of hashrefs defining all specific
1511 permissions currently defined.  The return value
1512 has the same structure as that of C<get_user_subpermissions>,
1513 except that the innermost hash value is the description
1514 of the subpermission.
1515
1516 =cut
1517
1518 sub get_all_subpermissions {
1519     my $dbh = C4::Context->dbh;
1520     my $sth = $dbh->prepare("SELECT flag, code, description
1521                              FROM permissions
1522                              JOIN userflags ON (module_bit = bit)");
1523     $sth->execute();
1524
1525     my $all_perms = {};
1526     while (my $perm = $sth->fetchrow_hashref) {
1527         $all_perms->{$perm->{'flag'}}->{$perm->{'code'}} = $perm->{'description'};
1528     }
1529     return $all_perms;
1530 }
1531
1532 =head2 haspermission
1533
1534   $flags = ($userid, $flagsrequired);
1535
1536 C<$userid> the userid of the member
1537 C<$flags> is a hashref of required flags like C<$borrower-&lt;{authflags}> 
1538
1539 Returns member's flags or 0 if a permission is not met.
1540
1541 =cut
1542
1543 sub haspermission {
1544     my ($userid, $flagsrequired) = @_;
1545     my $sth = C4::Context->dbh->prepare("SELECT flags FROM borrowers WHERE userid=?");
1546     $sth->execute($userid);
1547     my $flags = getuserflags( $sth->fetchrow(), $userid );
1548     if ( $userid eq C4::Context->config('user') ) {
1549         # Super User Account from /etc/koha.conf
1550         $flags->{'superlibrarian'} = 1;
1551     }
1552     elsif ( $userid eq 'demo' && C4::Context->config('demo') ) {
1553         # Demo user that can do "anything" (demo=1 in /etc/koha.conf)
1554         $flags->{'superlibrarian'} = 1;
1555     }
1556     return $flags if $flags->{superlibrarian};
1557     foreach my $module ( keys %$flagsrequired ) {
1558         my $subperm = $flagsrequired->{$module};
1559         if ($subperm eq '*') {
1560             return 0 unless ( $flags->{$module} == 1 or ref($flags->{$module}) );
1561         } else {
1562             return 0 unless ( $flags->{$module} == 1 or
1563                                 ( ref($flags->{$module}) and
1564                                   exists $flags->{$module}->{$subperm} and
1565                                   $flags->{$module}->{$subperm} == 1
1566                                 )
1567                             );
1568         }
1569     }
1570     return $flags;
1571     #FIXME - This fcn should return the failed permission so a suitable error msg can be delivered.
1572 }
1573
1574
1575 sub getborrowernumber {
1576     my ($userid) = @_;
1577     my $userenv = C4::Context->userenv;
1578     if ( defined( $userenv ) && ref( $userenv ) eq 'HASH' && $userenv->{number} ) {
1579         return $userenv->{number};
1580     }
1581     my $dbh = C4::Context->dbh;
1582     for my $field ( 'userid', 'cardnumber' ) {
1583         my $sth =
1584           $dbh->prepare("select borrowernumber from borrowers where $field=?");
1585         $sth->execute($userid);
1586         if ( $sth->rows ) {
1587             my ($bnumber) = $sth->fetchrow;
1588             return $bnumber;
1589         }
1590     }
1591     return 0;
1592 }
1593
1594 END { }    # module clean-up code here (global destructor)
1595 1;
1596 __END__
1597
1598 =head1 SEE ALSO
1599
1600 CGI(3)
1601
1602 C4::Output(3)
1603
1604 Digest::MD5(3)
1605
1606 =cut