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