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