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