Bug fixing and complete removal of Date::Manip
[koha.git] / C4 / Auth.pm
1 # -*- tab-width: 8 -*-
2 # NOTE: This file uses 8-character tabs; do not change the tab size!
3
4 package C4::Auth;
5
6 # Copyright 2000-2002 Katipo Communications
7 #
8 # This file is part of Koha.
9 #
10 # Koha is free software; you can redistribute it and/or modify it under the
11 # terms of the GNU General Public License as published by the Free Software
12 # Foundation; either version 2 of the License, or (at your option) any later
13 # version.
14 #
15 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
16 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
17 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License along with
20 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
21 # Suite 330, Boston, MA  02111-1307 USA
22
23 use strict;
24 use Digest::MD5 qw(md5_base64);
25
26 require Exporter;
27 use C4::Context;
28 use C4::Output;              # to get the template
29 use C4::Interface::CGI::Output;
30 use C4::Members;  # getpatroninformation
31 use C4::Koha;## to get branch
32 # use Net::LDAP;
33 # use Net::LDAP qw(:all);
34
35 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
36
37 # set the version for version checking
38 $VERSION = 0.01;
39 @ISA = qw(Exporter);
40
41 @EXPORT = qw(
42 &checkpw
43 );
44 =head1 NAME
45
46 C4::Auth - Authenticates Koha users
47
48 =head1 SYNOPSIS
49
50   use CGI;
51   use C4::Auth;
52
53   my $query = new CGI;
54
55   my ($template, $borrowernumber, $cookie) 
56     = get_template_and_user({template_name   => "opac-main.tmpl",
57                              query           => $query,
58                              type            => "opac",
59                              authnotrequired => 1,
60                              flagsrequired   => {borrow => 1},
61                           });
62
63   print $query->header(
64     -type => "text/html",
65     -charset=>"utf-8",
66     -cookie => $cookie
67   ), $template->output;
68
69
70 =head1 DESCRIPTION
71
72     The main function of this module is to provide
73     authentification. However the get_template_and_user function has
74     been provided so that a users login information is passed along
75     automatically. This gets loaded into the template.
76
77 =head1 FUNCTIONS
78
79 =over 2
80
81 =cut
82
83
84
85 @ISA = qw(Exporter);
86 @EXPORT = qw(
87              &checkauth
88              &get_template_and_user
89 );
90
91 =item get_template_and_user
92
93   my ($template, $borrowernumber, $cookie)
94     = get_template_and_user({template_name   => "opac-main.tmpl",
95                              query           => $query,
96                              type            => "opac",
97                              authnotrequired => 1,
98                              flagsrequired   => {borrow => 1},
99                           });
100
101     This call passes the C<query>, C<flagsrequired> and C<authnotrequired>
102     to C<&checkauth> (in this module) to perform authentification.
103     See C<&checkauth> for an explanation of these parameters.
104
105     The C<template_name> is then used to find the correct template for
106     the page. The authenticated users details are loaded onto the
107     template in the HTML::Template LOOP variable C<USER_INFO>. Also the
108     C<sessionID> is passed to the template. This can be used in templates
109     if cookies are disabled. It needs to be put as and input to every
110     authenticated page.
111
112     More information on the C<gettemplate> sub can be found in the
113     Output.pm module.
114
115 =cut
116
117
118 sub get_template_and_user {
119         my $in = shift;
120         my $template = gettemplate($in->{'template_name'}, $in->{'type'},$in->{'query'});
121         my ($user, $cookie, $sessionID, $flags)
122                 = checkauth($in->{'query'}, $in->{'authnotrequired'}, $in->{'flagsrequired'}, $in->{'type'});
123
124         my $borrowernumber;
125         if ($user) {
126                 $template->param(loggedinusername => $user);
127                 $template->param(sessionID => $sessionID);
128
129                 $borrowernumber = getborrowernumber($user);
130                 my ($borr, $alternativeflags) = getpatroninformation(undef, $borrowernumber);
131                 my @bordat;
132                 $bordat[0] = $borr;
133                 $template->param(USER_INFO => \@bordat,
134                 );
135                 my $branches=GetBranches();
136                 $template->param(branchname=>$branches->{$borr->{branchcode}}->{branchname},);
137                 
138                 # We are going to use the $flags returned by checkauth
139                 # to create the template's parameters that will indicate
140                 # which menus the user can access.
141                 if ($flags && $flags->{superlibrarian} == 1)
142                 {
143                         $template->param(CAN_user_circulate => 1);
144                         $template->param(CAN_user_catalogue => 1);
145                         $template->param(CAN_user_parameters => 1);
146                         $template->param(CAN_user_borrowers => 1);
147                         $template->param(CAN_user_permission => 1);
148                         $template->param(CAN_user_reserveforothers => 1);
149                         $template->param(CAN_user_borrow => 1);
150                         $template->param(CAN_user_reserveforself => 1);
151                         $template->param(CAN_user_editcatalogue => 1);
152                         $template->param(CAN_user_updatecharge => 1);
153                         $template->param(CAN_user_acquisition => 1);
154                         $template->param(CAN_user_management => 1);
155                         $template->param(CAN_user_tools => 1); }
156                 
157                 if ($flags && $flags->{circulate} == 1) {
158                         $template->param(CAN_user_circulate => 1); }
159
160                 if ($flags && $flags->{catalogue} == 1) {
161                         $template->param(CAN_user_catalogue => 1); }
162                 
163
164                 if ($flags && $flags->{parameters} == 1) {
165                         $template->param(CAN_user_parameters => 1);     
166                         $template->param(CAN_user_management => 1);
167                         $template->param(CAN_user_tools => 1); }
168                 
169
170                 if ($flags && $flags->{borrowers} == 1) {
171                         $template->param(CAN_user_borrowers => 1); }
172                 
173
174                 if ($flags && $flags->{permissions} == 1) {
175                         $template->param(CAN_user_permission => 1); }
176                 
177                 if ($flags && $flags->{reserveforothers} == 1) {
178                         $template->param(CAN_user_reserveforothers => 1); }
179                 
180
181                 if ($flags && $flags->{borrow} == 1) {
182                         $template->param(CAN_user_borrow => 1); }
183                 
184
185                 if ($flags && $flags->{reserveforself} == 1) {
186                         $template->param(CAN_user_reserveforself => 1); }
187                 
188
189                 if ($flags && $flags->{editcatalogue} == 1) {
190                         $template->param(CAN_user_editcatalogue => 1); }
191                 
192
193                 if ($flags && $flags->{updatecharges} == 1) {
194                         $template->param(CAN_user_updatecharge => 1); }
195                 
196                 if ($flags && $flags->{acquisition} == 1) {
197                         $template->param(CAN_user_acquisition => 1); }
198                 
199                 if ($flags && $flags->{management} == 1) {
200                         $template->param(CAN_user_management => 1);
201                         $template->param(CAN_user_tools => 1); }
202                 
203                 if ($flags && $flags->{tools} == 1) {
204                         $template->param(CAN_user_tools => 1); }
205                 
206         }
207         if  ($in->{'type'} eq "intranet") {
208         $template->param(
209                         intranetcolorstylesheet => C4::Context->preference("intranetcolorstylesheet"),  
210                         intranetstylesheet => C4::Context->preference("intranetstylesheet"),
211                         IntranetNav => C4::Context->preference("IntranetNav"),
212
213         );
214
215         }
216         else {
217         $template->param(
218                                 suggestion => C4::Context->preference("suggestion"),
219                                 virtualshelves => C4::Context->preference("virtualshelves"),
220                                 OpacNav => C4::Context->preference("OpacNav"),
221                                 opacheader      => C4::Context->preference("opacheader"),
222                                 opaccredits => C4::Context->preference("opaccredits"),
223                                 opacsmallimage => C4::Context->preference("opacsmallimage"),
224                                 opaclayoutstylesheet => C4::Context->preference("opaclayoutstylesheet"),
225                                 opaccolorstylesheet => C4::Context->preference("opaccolorstylesheet"),
226                                 opaclanguagesdisplay => C4::Context->preference("opaclanguagesdisplay"),
227                                 TemplateEncoding => C4::Context->preference("TemplateEncoding"),
228                                 opacuserlogin => C4::Context->preference("opacuserlogin"),
229                                 opacbookbag => C4::Context->preference("opacbookbag"),
230                 );
231         }
232         $template->param(
233                                 TemplateEncoding => C4::Context->preference("TemplateEncoding"),
234                                 AmazonContent => C4::Context->preference("AmazonContent"),
235                              LibraryName => C4::Context->preference("LibraryName"),
236                 );
237         return ($template, $borrowernumber, $cookie);
238 }
239
240
241 =item checkauth
242
243   ($userid, $cookie, $sessionID) = &checkauth($query, $noauth, $flagsrequired, $type);
244
245 Verifies that the user is authorized to run this script.  If
246 the user is authorized, a (userid, cookie, session-id, flags)
247 quadruple is returned.  If the user is not authorized but does
248 not have the required privilege (see $flagsrequired below), it
249 displays an error page and exits.  Otherwise, it displays the
250 login page and exits.
251
252 Note that C<&checkauth> will return if and only if the user
253 is authorized, so it should be called early on, before any
254 unfinished operations (e.g., if you've opened a file, then
255 C<&checkauth> won't close it for you).
256
257 C<$query> is the CGI object for the script calling C<&checkauth>.
258
259 The C<$noauth> argument is optional. If it is set, then no
260 authorization is required for the script.
261
262 C<&checkauth> fetches user and session information from C<$query> and
263 ensures that the user is authorized to run scripts that require
264 authorization.
265
266 The C<$flagsrequired> argument specifies the required privileges
267 the user must have if the username and password are correct.
268 It should be specified as a reference-to-hash; keys in the hash
269 should be the "flags" for the user, as specified in the Members
270 intranet module. Any key specified must correspond to a "flag"
271 in the userflags table. E.g., { circulate => 1 } would specify
272 that the user must have the "circulate" privilege in order to
273 proceed. To make sure that access control is correct, the
274 C<$flagsrequired> parameter must be specified correctly.
275
276 The C<$type> argument specifies whether the template should be
277 retrieved from the opac or intranet directory tree.  "opac" is
278 assumed if it is not specified; however, if C<$type> is specified,
279 "intranet" is assumed if it is not "opac".
280
281 If C<$query> does not have a valid session ID associated with it
282 (i.e., the user has not logged in) or if the session has expired,
283 C<&checkauth> presents the user with a login page (from the point of
284 view of the original script, C<&checkauth> does not return). Once the
285 user has authenticated, C<&checkauth> restarts the original script
286 (this time, C<&checkauth> returns).
287
288 The login page is provided using a HTML::Template, which is set in the
289 systempreferences table or at the top of this file. The variable C<$type>
290 selects which template to use, either the opac or the intranet 
291 authentification template.
292
293 C<&checkauth> returns a user ID, a cookie, and a session ID. The
294 cookie should be sent back to the browser; it verifies that the user
295 has authenticated.
296
297 =cut
298
299
300
301 sub checkauth {
302         my $query=shift;
303         # $authnotrequired will be set for scripts which will run without authentication
304         my $authnotrequired = shift;
305         my $flagsrequired = shift;
306         my $type = shift;
307         $type = 'opac' unless $type;
308
309         my $dbh = C4::Context->dbh;
310         my $timeout = C4::Context->preference('timeout');
311         $timeout = 600 unless $timeout;
312
313         my $template_name;
314         if ($type eq 'opac') {
315                 $template_name = "opac-auth.tmpl";
316         } else {
317                 $template_name = "auth.tmpl";
318         }
319
320         # state variables
321         my $loggedin = 0;
322         my %info;
323         my ($userid, $cookie, $sessionID, $flags,$envcookie);
324         my $logout = $query->param('logout.x');
325         if ($userid = $ENV{'REMOTE_USER'}) {
326                 # Using Basic Authentication, no cookies required
327                 $cookie=$query->cookie(-name => 'sessionID',
328                                 -value => '',
329                                 -expires => '');
330                 $loggedin = 1;
331         } elsif ($sessionID=$query->cookie('sessionID')) {
332                 C4::Context->_new_userenv($sessionID);
333                 if (my %hash=$query->cookie('userenv')){
334                                 C4::Context::set_userenv(
335                                         $hash{number},
336                                         $hash{id},
337                                         $hash{cardnumber},
338                                         $hash{firstname},
339                                         $hash{surname},
340                                         $hash{branch},
341                                         $hash{branchname},
342                                         $hash{flags},
343                                         $hash{emailaddress},
344                                 );
345                 }
346                 my ($ip , $lasttime);
347
348                 ($userid, $ip, $lasttime) = $dbh->selectrow_array(
349                                 "SELECT userid,ip,lasttime FROM sessions WHERE sessionid=?",
350                                                                 undef, $sessionID);
351                 if ($logout) {
352                 # voluntary logout the user
353                 $dbh->do("DELETE FROM sessions WHERE sessionID=?", undef, $sessionID);
354                 C4::Context->_unset_userenv($sessionID);
355                 $sessionID = undef;
356                 $userid = undef;
357                 open L, ">>/tmp/sessionlog";
358                 my $time=localtime(time());
359                 printf L "%20s from %16s logged out at %30s (manually).\n", $userid, $ip, $time;
360                 close L;
361                 }
362                 if ($userid) {
363                         if ($lasttime<time()-$timeout) {
364                                 # timed logout
365                                 $info{'timed_out'} = 1;
366                                 $dbh->do("DELETE FROM sessions WHERE sessionID=?", undef, $sessionID);
367                                 C4::Context->_unset_userenv($sessionID);
368                                 $userid = undef;
369                                 $sessionID = undef;
370                                 open L, ">>/tmp/sessionlog";
371                                 my $time=localtime(time());
372                                 printf L "%20s from %16s logged out at %30s (inactivity).\n", $userid, $ip, $time;
373                                 close L;
374                         } elsif ($ip ne $ENV{'REMOTE_ADDR'}) {
375                                 # Different ip than originally logged in from
376                                 $info{'oldip'} = $ip;
377                                 $info{'newip'} = $ENV{'REMOTE_ADDR'};
378                                 $info{'different_ip'} = 1;
379                                 $dbh->do("DELETE FROM sessions WHERE sessionID=?", undef, $sessionID);
380                                 C4::Context->_unset_userenv($sessionID);
381                                 $sessionID = undef;
382                                 $userid = undef;
383                                 open L, ">>/tmp/sessionlog";
384                                 my $time=localtime(time());
385                                 printf L "%20s from logged out at %30s (ip changed from %16s to %16s).\n", $userid, $time, $ip, $info{'newip'};
386                                 close L;
387                         } else {
388                                 $cookie=$query->cookie(-name => 'sessionID',
389                                                 -value => $sessionID,
390                                                 -expires => '');
391                                 $dbh->do("UPDATE sessions SET lasttime=? WHERE sessionID=?",
392                                         undef, (time(), $sessionID));
393                                 $flags = haspermission($dbh, $userid, $flagsrequired);
394                                 if ($flags) {
395                                 $loggedin = 1;
396                                 } else {
397                                 $info{'nopermission'} = 1;
398                                 }
399                         }
400                 }
401         }
402         unless ($userid) {
403                 $sessionID=int(rand()*100000).'-'.time();
404                 $userid=$query->param('userid');
405                 my $password=$query->param('password');
406                 C4::Context->_new_userenv($sessionID);
407                 my ($return, $cardnumber) = checkpw($dbh,$userid,$password);
408                 if ($return) {
409                         $dbh->do("DELETE FROM sessions WHERE sessionID=? AND userid=?",
410                                 undef, ($sessionID, $userid));
411                         $dbh->do("INSERT INTO sessions (sessionID, userid, ip,lasttime) VALUES (?, ?, ?, ?)",
412                                 undef, ($sessionID, $userid, $ENV{'REMOTE_ADDR'}, time()));
413                         open L, ">>/tmp/sessionlog";
414                         my $time=localtime(time());
415                         printf L "%20s from %16s logged in  at %30s.\n", $userid, $ENV{'REMOTE_ADDR'}, $time;
416                         close L;
417                         $cookie=$query->cookie(-name => 'sessionID',
418                                                 -value => $sessionID,
419                                                 -expires => '');
420                         if ($flags = haspermission($dbh, $userid, $flagsrequired)) {
421                                 $loggedin = 1;
422                         } else {
423                                 $info{'nopermission'} = 1;
424                                         C4::Context->_unset_userenv($sessionID);
425                         }
426                         if ($return == 1){
427                                 my ($bornum,$firstname,$surname,$userflags,$branchcode,$branchname,$emailaddress);
428                                 my $sth=$dbh->prepare("select borrowernumber,firstname,surname,flags,borrowers.branchcode,branchname,emailaddress from borrowers left join branches on borrowers.branchcode=branches.branchcode where userid=?");
429                                 $sth->execute($userid);
430                                 ($bornum,$firstname,$surname,$userflags,$branchcode,$branchname, $emailaddress) = $sth->fetchrow if ($sth->rows);
431 #                               warn "$cardnumber,$bornum,$userid,$firstname,$surname,$userflags,$branchcode,$emailaddress";
432                                 unless ($sth->rows){
433                                         my $sth=$dbh->prepare("select borrowernumber,firstname,surname,flags,borrowers.branchcode,branchname,emailaddress from borrowers left join branches on borrowers.branchcode=branches.branchcode where cardnumber=?");
434                                         $sth->execute($cardnumber);
435                                         ($bornum,$firstname,$surname,$userflags,$branchcode, $branchname,$emailaddress) = $sth->fetchrow if ($sth->rows);
436 #                                       warn "$cardnumber,$bornum,$userid,$firstname,$surname,$userflags,$branchcode,$emailaddress";
437                                         unless ($sth->rows){
438                                                 $sth->execute($userid);
439                                                 ($bornum,$firstname,$surname,$userflags,$branchcode, $branchname, $emailaddress) = $sth->fetchrow if ($sth->rows);
440                                         }
441 #                                       warn "$cardnumber,$bornum,$userid,$firstname,$surname,$userflags,$branchcode,$emailaddress";
442                                 }
443                                 my $hash = C4::Context::set_userenv(
444                                         $bornum,
445                                         $userid,
446                                         $cardnumber,
447                                         $firstname,
448                                         $surname,
449                                         $branchcode,
450                                         $branchname, 
451                                         $userflags,
452                                         $emailaddress,
453                                 );
454 #                               warn "$cardnumber,$bornum,$userid,$firstname,$surname,$userflags,$branchcode,$emailaddress";
455                                 $envcookie=$query->cookie(-name => 'userenv',
456                                                 -value => $hash,
457                                                 -expires => '');
458                         } elsif ($return == 2) {
459                         #We suppose the user is the superlibrarian
460                                 my $hash = C4::Context::set_userenv(
461                                         0,0,
462                                         C4::Context->config('user'),
463                                         C4::Context->config('user'),
464                                         C4::Context->config('user'),
465                                         "","",1,C4::Context->preference('KohaAdminEmailAddress')
466                                 );
467                                 $envcookie=$query->cookie(-name => 'userenv',
468                                                 -value => $hash,
469                                                 -expires => '');
470                         }
471                 } else {
472                         if ($userid) {
473                                 $info{'invalid_username_or_password'} = 1;
474                                 C4::Context->_unset_userenv($sessionID);
475                         }
476                 }
477         }
478         my $insecure = C4::Context->boolean_preference('insecure');
479         # finished authentification, now respond
480         if ($loggedin || $authnotrequired || (defined($insecure) && $insecure)) {
481                 # successful login
482                 unless ($cookie) {
483                 $cookie=$query->cookie(-name => 'sessionID',
484                                         -value => '',
485                                         -expires => '');
486                 }
487                 if ($envcookie){
488                         return ($userid, [$cookie,$envcookie], $sessionID, $flags)
489                 } else {
490                         return ($userid, $cookie, $sessionID, $flags);
491                 }
492         }
493         # else we have a problem...
494         # get the inputs from the incoming query
495         my @inputs =();
496         foreach my $name (param $query) {
497                 (next) if ($name eq 'userid' || $name eq 'password');
498                 my $value = $query->param($name);
499                 push @inputs, {name => $name , value => $value};
500         }
501
502         my $template = gettemplate($template_name, $type,$query);
503         $template->param(INPUTS => \@inputs,
504                         intranetcolorstylesheet => C4::Context->preference("intranetcolorstylesheet"),
505                         intranetstylesheet => C4::Context->preference("intranetstylesheet"),
506                         IntranetNav => C4::Context->preference("IntranetNav"),
507                         opacnav => C4::Context->preference("OpacNav"),
508                         TemplateEncoding => C4::Context->preference("TemplateEncoding"),
509
510                         );
511         $template->param(loginprompt => 1) unless $info{'nopermission'};
512
513         my $self_url = $query->url(-absolute => 1);
514         $template->param(url => $self_url, LibraryName=> => C4::Context->preference("LibraryName"),);
515         $template->param(\%info);
516         $cookie=$query->cookie(-name => 'sessionID',
517                                         -value => $sessionID,
518                                         -expires => '');
519         print $query->header(
520                 -type => "text/html",
521                 -charset=>"utf-8",
522                 -cookie => $cookie
523                 ), $template->output;
524         exit;
525 }
526
527
528
529
530 sub checkpw {
531
532         my ($dbh, $userid, $password) = @_;
533 # INTERNAL AUTH
534         my $sth=$dbh->prepare("select password,cardnumber from borrowers where userid=?");
535         $sth->execute($userid);
536         if ($sth->rows) {
537                 my ($md5password,$cardnumber) = $sth->fetchrow;
538                 if (md5_base64($password) eq $md5password) {
539                         return 1,$cardnumber;
540                 }
541         }
542         my $sth=$dbh->prepare("select password from borrowers where cardnumber=?");
543         $sth->execute($userid);
544         if ($sth->rows) {
545                 my ($md5password) = $sth->fetchrow;
546                 if (md5_base64($password) eq $md5password) {
547                         return 1,$userid;
548                 }
549         }
550         if ($userid eq C4::Context->config('user') && $password eq C4::Context->config('pass')) {
551                 # Koha superuser account
552                 return 2;
553         }
554         if ($userid eq 'demo' && $password eq 'demo' && C4::Context->config('demo')) {
555                 # DEMO => the demo user is allowed to do everything (if demo set to 1 in koha.conf
556                 # some features won't be effective : modify systempref, modify MARC structure,
557                 return 2;
558         }
559         return 0;
560 }
561
562 sub getuserflags {
563     my $cardnumber=shift;
564     my $dbh=shift;
565     my $userflags;
566     my $sth=$dbh->prepare("SELECT flags FROM borrowers WHERE cardnumber=?");
567     $sth->execute($cardnumber);
568     my ($flags) = $sth->fetchrow;
569     $sth=$dbh->prepare("SELECT bit, flag, defaulton FROM userflags");
570     $sth->execute;
571     while (my ($bit, $flag, $defaulton) = $sth->fetchrow) {
572         if (($flags & (2**$bit)) || $defaulton) {
573             $userflags->{$flag}=1;
574         }
575     }
576     return $userflags;
577 }
578
579 sub haspermission {
580     my ($dbh, $userid, $flagsrequired) = @_;
581     my $sth=$dbh->prepare("SELECT cardnumber FROM borrowers WHERE userid=?");
582     $sth->execute($userid);
583     my ($cardnumber) = $sth->fetchrow;
584     ($cardnumber) || ($cardnumber=$userid);
585     my $flags=getuserflags($cardnumber,$dbh);
586     my $configfile;
587     if ($userid eq C4::Context->config('user')) {
588         # Super User Account from /etc/koha.conf
589         $flags->{'superlibrarian'}=1;
590      }
591      if ($userid eq 'demo' && C4::Context->config('demo')) {
592         # Demo user that can do "anything" (demo=1 in /etc/koha.conf)
593         $flags->{'superlibrarian'}=1;
594     }
595     return $flags if $flags->{superlibrarian};
596     foreach (keys %$flagsrequired) {
597         return $flags if $flags->{$_};
598     }
599     return 0;
600 }
601
602 sub getborrowernumber {
603     my ($userid) = @_;
604     my $dbh = C4::Context->dbh;
605     for my $field ('userid', 'cardnumber') {
606       my $sth=$dbh->prepare
607           ("select borrowernumber from borrowers where $field=?");
608       $sth->execute($userid);
609       if ($sth->rows) {
610         my ($bnumber) = $sth->fetchrow;
611         return $bnumber;
612       }
613     }
614     return 0;
615 }
616
617 END { }       # module clean-up code here (global destructor)
618 1;
619 __END__
620
621 =back
622
623 =head1 SEE ALSO
624
625 CGI(3)
626
627 C4::Output(3)
628
629 Digest::MD5(3)
630
631 =cut