diff options
| author | fukachan <fukachan> | 2008-09-09 08:48:59 +0000 |
|---|---|---|
| committer | fukachan <fukachan> | 2008-09-09 08:48:59 +0000 |
| commit | 453db284ee46f8c24e2d28a6ad493d1175535b1f (patch) | |
| tree | f684b32276616fd21f4c51b9bf60bc84bf6e4697 /fml/lib/FML | |
| parent | 0ecdfd221c3b6ae46065f2d0becc6effd2c524bf (diff) | |
| download | fml8-453db284ee46f8c24e2d28a6ad493d1175535b1f.tar.gz fml8-453db284ee46f8c24e2d28a6ad493d1175535b1f.tar.bz2 fml8-453db284ee46f8c24e2d28a6ad493d1175535b1f.zip | |
anonymous cgi initial version
Diffstat (limited to 'fml/lib/FML')
| -rw-r--r-- | fml/lib/FML/CGI/Anonymous/DB.pm | 270 | ||||
| -rw-r--r-- | fml/lib/FML/CGI/Anonymous/Submit.pm | 53 | ||||
| -rw-r--r-- | fml/lib/FML/CGI/Skin/Anonymous.pm | 558 |
3 files changed, 881 insertions, 0 deletions
diff --git a/fml/lib/FML/CGI/Anonymous/DB.pm b/fml/lib/FML/CGI/Anonymous/DB.pm new file mode 100644 index 00000000..598066f1 --- /dev/null +++ b/fml/lib/FML/CGI/Anonymous/DB.pm @@ -0,0 +1,270 @@ +#-*- perl -*- +# +# Copyright (C) 2008 Ken'ichi Fukamachi +# All rights reserved. This program is free software; you can +# redistribute it and/or modify it under the same terms as Perl itself. +# +# $FML: @template.pm,v 1.12 2008/08/24 08:28:36 fukachan Exp $ +# + +package FML::CGI::Anonymous::DB; +use strict; +use vars qw(@ISA @EXPORT @EXPORT_OK $AUTOLOAD); +use Carp; + +=head1 NAME + +FML::CGI::Anonymous::DB - anonymous user database. + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +=head1 METHODS + +=head2 new() + +constructor. + +=cut + + +# Descriptions: constructor. +# Arguments: OBJ($self) OBJ($curproc) +# Side Effects: none +# Return Value: OBJ +sub new +{ + my ($self, $curproc) = @_; + my ($type) = ref($self) || $self; + my $me = { _curproc => $curproc }; + return bless $me, $type; +} + + +# Descriptions: assign a magic string and an identifier. +# see FML::CGI::Skin::Anonymous::run_cgi_main() +# for more details. +# Arguments: OBJ($self) +# Side Effects: update PCB and database. +# Return Value: none +sub assign_id +{ + my ($self) = @_; + my ($curproc) = $self->{ _curproc }; + my ($config) = $curproc->config(); + my ($pcb) = $curproc->pcb(); + + # generate a session identifier and a challenge magic string. + use FML::String::Random; + my $string = new FML::String::Random; + my $magic_string = $string->magic_string(); + my $session_id = $string->identifier($magic_string); + $self->set_session_id($session_id); + $self->set_magic_string($magic_string); + + # save them and the request into the temporal database => { + # session_id-$session_id => $session_id, + # magic_string-$session_id => $magic_string. + # }; + my $expire = $config->as_second("anonymous_cgi_expire_limit") || 300; + my $confirm = $self->_db_open(); + $confirm->set($session_id, "session_id", $session_id); + $confirm->set($session_id, "magic_string", $magic_string); + $confirm->set($session_id, "expire_time", time + $expire); +} + + +=head1 UTILITIES + +=head2 set_session_id($session_id) + +save the current session_id in PCB. + +=head2 get_session_id() + +get the current session_id. + +=head2 set_magic_string($magic_string) + +save the current magic string in PCB. + +=head2 get_magic_string() + +get the current magic string. + +=cut + + +# Descriptions: save session_id in PCB. +# Arguments: OBJ($self) STR($session_id) +# Side Effects: update pcb. +# Return Value: none +sub set_session_id +{ + my ($self, $session_id) = @_; + my ($curproc) = $self->{ _curproc }; + my ($pcb) = $curproc->pcb(); + + $pcb->set("cgi", "anonymous_session_id", $session_id); +} + + +# Descriptions: get the current session_id in PCB. +# Arguments: OBJ($self) +# Side Effects: none +# Return Value: STR +sub get_session_id +{ + my ($self) = @_; + my ($curproc) = $self->{ _curproc }; + my ($pcb) = $curproc->pcb(); + + if (defined $pcb) { + return $pcb->get("cgi", "anonymous_session_id"); + } + else { + $curproc->logerror("get_session_id: no pcb"); + return undef; + } +} + + +# Descriptions: save magic string in PCB. +# Arguments: OBJ($self) STR($magic_string) +# Side Effects: update pcb. +# Return Value: none +sub set_magic_string +{ + my ($self, $magic_string) = @_; + my ($curproc) = $self->{ _curproc }; + my ($pcb) = $curproc->pcb(); + + $pcb->set("cgi", "magic_string", $magic_string); +} + + +# Descriptions: get the current magic string in PCB. +# Arguments: OBJ($self) +# Side Effects: none +# Return Value: STR +sub get_magic_string +{ + my ($self) = @_; + my ($curproc) = $self->{ _curproc }; + my ($pcb) = $curproc->pcb(); + + if (defined $pcb) { + return $pcb->get("cgi", "magic_string"); + } + else { + $curproc->logerror("get_magic_string: no pcb"); + return undef; + } +} + + +# Descriptions: check if the request for this session_id is too old ? +# Arguments: OBJ($self) STR($session_id) +# Side Effects: none +# Return Value: NUM(1 or 0) +sub is_expired +{ + my ($self, $session_id) = @_; + my $confirm = $self->_db_open(); + my $expire_time = $confirm->get($session_id, "expire_time") || 0; + + return( time > $expire_time ? 1 : 0 ); +} + + +# Descriptions: check if the $magic_string is correct for $session_id. +# Arguments: OBJ($self) STR($session_id) STR($magic_string) +# Side Effects: none +# Return Value: NUM(1 or 0) +sub is_correct_magic_string +{ + my ($self, $session_id, $magic_string) = @_; + my $confirm = $self->_db_open(); + my $db_session_id = $confirm->get($session_id, "session_id"); + my $db_magic_string = $confirm->get($session_id, "magic_string"); + + # case insensitive + $magic_string =~ tr/A-Z/a-z/; + $db_magic_string =~ tr/A-Z/a-z/; + if ($magic_string eq $db_magic_string) { + return 1; + } + else { + return 0; + } +} + + +=head1 DATABASE OPERATIONS + +private. + +DO NOT USE THESE METHODS. + +=cut + + +# Descriptions: open the database. +# Arguments: OBJ($self) +# Side Effects: none +# Return Value: OBJ +sub _db_open +{ + my ($self) = @_; + my ($curproc) = $self->{ _curproc }; + my ($config) = $curproc->config(); + my ($pcb) = $curproc->pcb(); + + use FML::Confirm; + my $cache_dir = $config->{ db_dir }; + my $confirm = new FML::Confirm $curproc, { + keyword => "confirm", + cache_dir => $cache_dir, + class => "cgi", + address => "dummy", + buffer => "dummy", + }; + return $confirm; +} + + +# Descriptions: close the database (dummy). +# Arguments: OBJ($self) +# Side Effects: none +# Return Value: none +sub _db_close +{ + my ($self) = @_; +} + + +=head1 CODING STYLE + +See C<http://www.fml.org/software/FNF/> on fml coding style guide. + +=head1 AUTHOR + +Ken'ichi Fukamachi + +=head1 COPYRIGHT + +Copyright (C) 2008 Ken'ichi Fukamachi + +All rights reserved. This program is free software; you can +redistribute it and/or modify it under the same terms as Perl itself. + +=head1 HISTORY + +FML::CGI::Anonymous::DB appeared in fml8 mailing list driver package. +See C<http://www.fml.org/> for more details. + +=cut + + +1; diff --git a/fml/lib/FML/CGI/Anonymous/Submit.pm b/fml/lib/FML/CGI/Anonymous/Submit.pm new file mode 100644 index 00000000..05679da2 --- /dev/null +++ b/fml/lib/FML/CGI/Anonymous/Submit.pm @@ -0,0 +1,53 @@ +#-*- perl -*- +# +# Copyright (C) 2008 Ken'ichi Fukamachi +# All rights reserved. This program is free software; you can +# redistribute it and/or modify it under the same terms as Perl itself. +# +# $FML: @template.pm,v 1.12 2008/08/24 08:28:36 fukachan Exp $ +# + +package FML::CGI::Anonymous::Submit; +use strict; +use Carp; +use vars qw(@ISA @EXPORT @EXPORT_OK $AUTOLOAD); +use CGI qw/:standard/; # load standard CGI routines + +use FML::CGI::Skin::Anonymous; +@ISA = qw(FML::CGI::Skin::Anonymous); + + +=head1 NAME + +FML::CGI::Anonymous::Submit - submit a request from an anonymous user. + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +=head1 METHODS + +=head1 CODING STYLE + +See C<http://www.fml.org/software/FNF/> on fml coding style guide. + +=head1 AUTHOR + +Ken'ichi Fukamachi + +=head1 COPYRIGHT + +Copyright (C) 2008 Ken'ichi Fukamachi + +All rights reserved. This program is free software; you can +redistribute it and/or modify it under the same terms as Perl itself. + +=head1 HISTORY + +FML::CGI::Anonymous::Submit appeared in fml8 mailing list driver package. +See C<http://www.fml.org/> for more details. + +=cut + + +1; diff --git a/fml/lib/FML/CGI/Skin/Anonymous.pm b/fml/lib/FML/CGI/Skin/Anonymous.pm new file mode 100644 index 00000000..b823d210 --- /dev/null +++ b/fml/lib/FML/CGI/Skin/Anonymous.pm @@ -0,0 +1,558 @@ +#-*- perl -*- +# +# Copyright (C) 2008 Ken'ichi Fukamachi +# All rights reserved. This program is free software; you can +# redistribute it and/or modify it under the same terms as Perl itself. +# +# $FML$ +# + +package FML::CGI::Skin::Anonymous; +use strict; +use Carp; +use vars qw(@ISA @EXPORT @EXPORT_OK $AUTOLOAD $global_error_reason); +use CGI qw/:standard/; # load standard CGI routines + +use FML::Process::CGI; +@ISA = qw(FML::Process::CGI); + +my $debug = 0; + + +=head1 NAME + +FML::CGI::Skin::Anonymous - provides CGI control function for anonymous users. + +=head1 SYNOPSIS + + $obj = new FML::CGI::Skin::Anonymous; + $obj->prepare(); + $obj->verify_request(); + $obj->run(); + $obj->finish(); + +run() executes html_start(), run_cgi() and html_end() described below. + +See L<FML::Process::Flow> for flow details. + +=head1 DESCRIPTION + +=head2 CLASS HIERARCHY + +C<FML::CGI::Skin::Anonymous> is a subclass of C<FML::Process::CGI>. + + FML::Process::Kernel + | + A + FML::Process::CGI::Kernel + | + A + FML::Process::CGI + | + A + ----------------------- + | | + A A + FML::CGI::Skin::Anonymous + +=head1 METHODS + +Almost cgi common methods are forwarded to +C<FML::Process::CGI> base class. + +This module has routines needed for the admin CGI. + +=cut + + +# Descriptions: print out HTML header + body former part. +# Arguments: OBJ($curproc) +# Side Effects: none +# Return Value: none +sub html_start +{ + my ($curproc) = @_; + my $config = $curproc->config(); + my $myname = $curproc->cgi_var_myname(); + my $ml_name = $curproc->cgi_var_ml_name(); + my $ml_domain = $curproc->cgi_var_ml_domain(); + my $name_ui = $curproc->message_nl('cgi.anonymous.top'); + my $title = "${ml_name}\@${ml_domain} $name_ui"; + my $color = $config->{ cgi_main_menu_color } || '#FFFFFF'; + my $charset = $curproc->langinfo_get_charset("cgi"); + + # o.k start html + print start_html(-title => $title, + -lang => $charset, + -BGCOLOR => $color); + print "\n"; +} + + +# Descriptions: print out body latter part. +# Arguments: OBJ($curproc) +# Side Effects: none +# Return Value: none +sub html_end +{ + my ($curproc) = @_; + + # o.k. end of html + print end_html; + print "\n"; +} + + +# Descriptions: main routine for CGI. +# kick off suitable FML::Command finally +# via cgi_execulte_command(). +# Arguments: OBJ($curproc) +# Side Effects: none +# Return Value: none +sub run_cgi_main +{ + my ($curproc) = @_; + my $config = $curproc->config(); + my $address = $curproc->cgi_try_get_address(); + my $ml_name = $curproc->cgi_var_ml_name(); + my $pcb = $curproc->pcb(); + my $mode = 'anonymous'; # cgi needs to run for an anonymous user. + + # specified command, we need to identify + # the command specifined in the cgi_navigation and cgi_mein. + my $navi_command = $curproc->safe_param_navi_command() || ''; + my $command = $curproc->safe_param_command() || ''; + + # updat config: $ml_name is found now (get $ml_name from CGI). + $config->set('ml_name', $ml_name); + + if ($debug) { + print "<PRE>\n"; + print "ml_name = $ml_name\n"; + print "command = $command\n"; + print "navi_command = $navi_command\n"; + print "</PRE>\n"; + } + + # [I] 1st challenge phase ($curproc->prepare() has done it already). + # 1. save request and information before verification. + # 1.1 assign a hidden id for this request. + # 1.2 generate a challenge magic string (ALPHABET+, up to 8 chars). + # 1.3 save them and the request into the temporal database. + # bind the challenge password with the id + # for the later reverse operation. + # + # 2. show menu by $curproc->run_cgi_menu() at the center of table. + # + # [II] 2nd challenge phase: something is requested by an anonymou user. + # 3. check if this request is from a human or a spam robot ? + # 3.1 use the challenge password (assigned at 1.2 above). + my $is_ok = $curproc->_anonymous_is_human_operating(); + if ($is_ok) { + # if confirmed that a human seems operating, process the request. + $curproc->_anonymous_run_cgi_main(); + } + else { + if (defined $global_error_reason && $global_error_reason) { + my $buf = $curproc->message_nl("cgi.fail", + "Error! request fails."); + print "<p>", $buf, "<br>\n"; + } + + $curproc->logdebug("first time"); + } +} + + +# Descriptions: check if the request is operated by a human. +# Arguments: OBJ($curproc) +# Side Effects: none +# Return Value: NUM(1 or 0) +sub _anonymous_is_human_operating +{ + my ($curproc) = @_; + + if ($curproc->_anonymous_cgi_is_first_time()) { + return 0; + } + else { + my $session_id = $curproc->safe_param_session_id(); + my $magic_string = $curproc->safe_param_magic_string(); + if ($session_id && $magic_string) { + $curproc->log("session_id = $session_id"); + $curproc->logdebug("magic_string = $magic_string"); + } + else { + $curproc->logwarn("invalid request"); + return 0; + } + + # check the magic string is valid or not. return 1 if ok. + use FML::CGI::Anonymous::DB; + my $db = new FML::CGI::Anonymous::DB $curproc; + + # 1. expired ? + if ($db->is_expired($session_id)) { + $curproc->logerror("expired request"); + $global_error_reason = "expired"; + return 0; + } + + # 2. not expired, ok, check if the magic string is correct. + if ($db->is_correct_magic_string($session_id, $magic_string)) { + $curproc->log("correct magic string"); + return 1; + } + else { + $global_error_reason = "incorrect_magic_string"; + $curproc->logwarn("incorrect magic string"); + return 0; + } + } +} + + +# Descriptions: main routine for CGI. +# kick off suitable FML::Command finally +# via cgi_execulte_command(). +# Arguments: OBJ($curproc) +# Side Effects: none +# Return Value: none +sub _anonymous_run_cgi_main +{ + my ($curproc) = @_; + my $config = $curproc->config(); + my $pcb = $curproc->pcb(); + my $mode = 'user'; + + my $ml_name = $curproc->cgi_var_ml_name(); + my $address = $curproc->cgi_try_get_address(); + my $navi_command = $curproc->safe_param_navi_command() || ''; + my $command = $curproc->safe_param_command() || ''; + + # updat config: $ml_name is found now (get $ml_name from CGI). + $config->set('ml_name', $ml_name); + + if ($debug) { + print "<PRE>\n"; + print "ml_name = $ml_name\n"; + print "command = $command\n"; + print "navi_command = $navi_command\n"; + print "address = $address\n"; + print "</PRE>\n"; + } + + if ($command && $address) { + print "<br>* case 2 <br>\n" if $debug; + + my $command_context = + $curproc->command_context_init($command); + $command_context->set_mode("User"); + $command_context->set_ml_name($ml_name); + $command_context->set_data($address); + $command_context->set_options( [ $address ] ); + + # XXX we need a fake that we handle a mail request. + my $cred = $curproc->credential(); + $cred->set_sender($address); + $curproc->set_allow_reply_message(); + + $pcb->set('cgi', 'command_context', $command_context); + $curproc->cgi_execute_command($command_context, "ml_anonymous_cgi"); + } + else { + $curproc->log("cgi_main: invalid condition, not processed."); + $pcb->set('cgi', 'command_context', undef); + } +} + + +# Descriptions: show menu (table based menu). +# Arguments: OBJ($curproc) HASH_REF($args) +# Side Effects: none +# Return Value: none +sub run_cgi_navigator +{ + my ($curproc, $args) = @_; + my $target = $curproc->cgi_var_frame_target(); + my $action = $curproc->cgi_var_action(); + + # natural language-ed name + my $name_ml_name = $curproc->message_nl('term.ml_name', 'ml_name'); + my $name_command = $curproc->message_nl('term.command', 'command'); + my $name_submit = $curproc->message_nl('term.submit', 'submit'); + my $name_reset = $curproc->message_nl('term.reset', 'reset'); + + # 1. ML + # my $title = $curproc->cgi_var_navigator_title(); + # print $title, "\n"; + + # 2. command + my $usage = "anonymous usage"; + my $name_usage = $curproc->message_nl('cgi.anonymous.usage', $usage); + print $name_usage; + print "\n<BR>\n"; + + # 3. submit + # print submit(-name => $name_submit); + # print reset(-name => $name_reset); +} + + +=head2 run_cgi_menu() + +execute cgi_menu() given as FML::Command::* class. +The menu function should be within FML::Command::User::* class. + + for my $command ($commands_for_ml_anonymous_cgi) { + if $command in $anonymous_cgi_allowed_commands { + FML::Command::User::$command->cgi_menu(); + } + } + +=cut + + +# Descriptions: show the main menu at the screen center. +# Arguments: OBJ($curproc) +# Side Effects: load module +# Return Value: none +sub run_cgi_menu +{ + my ($curproc) = @_; + my $msg_args = $curproc->_gen_msg_args(); + + # top level help message + print $curproc->message_nl("cgi.anonymous.top", "", $msg_args); + + if ($curproc->_anonymous_cgi_is_first_time()) { + $curproc->_anonymous_cgi_print_menu(); + $curproc->_anonymous_cgi_print_magic_string(); + } + else { + ; + } +} + + +# Descriptions: check if this request is done at the first time. +# Arguments: OBJ($curproc) +# Side Effects: none +# Return Value: NUM(1 or 0) +sub _anonymous_cgi_is_first_time +{ + my ($curproc) = @_; + + my $session_id = $curproc->safe_param_session_id(); + return( $session_id ? 0 : 1 ); +} + + +# Descriptions: show menu for an anonymous user. +# Arguments: OBJ($curproc) +# Side Effects: none +# Return Value: none +sub _anonymous_cgi_print_menu +{ + my ($curproc) = @_; + my $ml_name = $curproc->cgi_var_ml_name(); + + for my $command (qw(subscribe unsubscribe)) { + my $pkg = sprintf("FML::Command::User::%s", $command); + my $command_context = { + command_mode => "user", + comname => $command, + command => $command, + ml_name => $ml_name, + options => [ ], + argv => undef, + args => undef, + }; + + my $eval = qq{ + use $pkg; + my \$command = new $pkg; + \$command->cgi_menu(\$curproc, \$command_context); + }; + eval $eval; + if ($@) { + print "$command not suport anonymous mode\n<br>\n"; + print "{$@}\n"; + } + } +} + + +# Descriptions: print the magic string. +# Arguments: OBJ($curproc) +# Side Effects: none +# Return Value: none +sub _anonymous_cgi_print_magic_string +{ + my ($curproc) = @_; + my $config = $curproc->config(); + my $_mgs = "magic string"; + my $name_magic = $curproc->message_nl('term.magic_string', $_mgs); + my $is_mode = "png"; + + use FML::CGI::Anonymous::DB; + my $db = new FML::CGI::Anonymous::DB $curproc; + my $string = $db->get_magic_string(); + my $session_id = $db->get_session_id(); + + use FML::String::Banner; + my $banner = new FML::String::Banner; + $banner->set_string($string); + if ($is_mode eq "ascii") { + my $ascii = $banner->as_ascii(); + printf("\n<pre>[%s]\n\n%s\n</pre>\n", $name_magic, $ascii); + } + elsif ($is_mode eq "png") { + my $html_tmp_dir = $config->get('html_tmp_dir'); + unless (-d $html_tmp_dir) { + mkdir $html_tmp_dir, 0755; + } + + use File::Spec; + my $png_filename = sprintf("%s.png", $session_id); + my $image_file = File::Spec->catfile($html_tmp_dir, $png_filename); + + use FileHandle; + my $wh = new FileHandle "> $image_file"; + if (defined $wh) { + $wh->binmode(); + my $png = $banner->as_png(); + $wh->print($png); + $wh->close(); + } + + my $url_base = $config->{ html_tmp_base_url }; + my $url = sprintf("%s/%s", $url_base, $png_filename); + printf("\n<p>%s\n\n<image src=\"%s\">\n", $name_magic, $url); + } +} + + +# Descriptions: dummy. +# Arguments: OBJ($curproc) +# Side Effects: none +# Return Value: none +sub run_cgi_title +{ + my ($curproc) = @_; +} + + +=head2 run_cgi_help() + +show help. + +=cut + + +# Descriptions: show help. +# Arguments: OBJ($curproc) +# Side Effects: none +# Return Value: none +sub run_cgi_help +{ + my ($curproc) = @_; + my $ml_name = $curproc->cgi_var_ml_name(); + my $ml_domain = $curproc->cgi_var_ml_domain(); + my $mode = $curproc->cgi_var_cgi_mode(); + my $msg_args = $curproc->_gen_msg_args(); + + print "<B>\n<CENTER>\n"; + print "$ml_name\@$ml_domain ML\n"; + print "</CENTER><BR>\n</B>\n"; +} + + +=head2 run_cgi_command_help() + +show command dependent help. + +=cut + + +# Descriptions: show command dependent help. +# Arguments: OBJ($curproc) +# Side Effects: none +# Return Value: none +sub run_cgi_command_help +{ + my ($curproc) = @_; + my $buf = ''; + my $navi_command = $curproc->safe_param_navi_command(); + my $command = $curproc->safe_param_command(); + my $msg_args = $curproc->_gen_msg_args(); + + # natural language-ed name + my $name_usage = $curproc->message_nl('term.usage', 'usage'); + + if ($navi_command) { + print "[$name_usage]<br> <b> $navi_command </b> <br>\n"; + $buf = $curproc->message_nl("cgi.config.$navi_command", '', $msg_args); + } + elsif ($command) { + print "[$name_usage]<br> <b> $command </b> <br>\n"; + $buf = $curproc->message_nl("cgi.config.$command", '', $msg_args); + } + + print $buf; +} + + +# Descriptions: prepare arguemnts for message handling. +# Arguments: OBJ($curproc) +# Side Effects: none +# Return Value: HASH_REF +sub _gen_msg_args +{ + my ($curproc) = @_; + + # natural language-ed name + my $name_submit = $curproc->message_nl('term.submit', 'submit'); + my $name_show = $curproc->message_nl('term.show', 'show'); + my $name_map = $curproc->message_nl('term.map', 'map'); + my $msg_args = { + _arg_button_submit => $name_submit, + _arg_button_show => $name_show, + _arg_scroll_map => $name_map, + }; + + return $msg_args; +} + + +=head1 SEE ALSO + +L<CGI>, +L<FML::Process::CGI> +and +L<FML::Process::Flow>. + +=head1 CODING STYLE + +See C<http://www.fml.org/software/FNF/> on fml coding style guide. + +=head1 AUTHOR + +Ken'ichi Fukamachi + +=head1 COPYRIGHT + +Copyright (C) 2008 Ken'ichi Fukamachi + +All rights reserved. This program is free software; you can +redistribute it and/or modify it under the same terms as Perl itself. + +=head1 HISTORY + +FML::CGI::Skin::Anonymous appeared in fml8 mailing list driver package. +See C<http://www.fml.org/> for more details. + +=cut + + +1; |
