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