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