Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # -*- Mode: perl -*-
- #
- # $Id: MxScreen.pm,v 0.1.1.1 2001/05/30 21:13:07 ram Exp $
- #
- # Copyright (c) 1998-2001, Raphael Manfredi
- # Copyright (c) 2000-2001, Christophe Dehaudt
- #
- # You may redistribute only under the terms of the Artistic License,
- # as specified in the README file that comes with the distribution.
- #
- # HISTORY
- # $Log: MxScreen.pm,v $
- # Revision 0.1.1.1 2001/05/30 21:13:07 ram
- # patch1: fixed HISTORY section
- # patch1: random cleanup in named argument docs
- # patch1: updated version number
- #
- # Revision 0.1 2001/04/22 17:57:03 ram
- # Baseline for first Alpha release.
- #
- # $EndLog$
- #
- use strict;
- package CGI::MxScreen;
- use vars qw($VERSION $BIN_VERSION);
- $VERSION = '0.103';
- $BIN_VERSION = '0.1';
- use CGI::MxScreen::Constant;
- use CGI::MxScreen::Error;
- use Carp::Datum;
- use Log::Agent;
- use Getargs::Long;
- use Time::HiRes qw(time);
- require CGI;
- require CGI::MxScreen::Form::Field;
- require CGI::MxScreen::Form::Button;
- require CGI::MxScreen::Layout;
- require CGI::MxScreen::Session;
- my @managers; # For END {}
- #
- # ->make
- #
- # Creation routine.
- #
- sub make {
- DFEATURE my $f_;
- my $self = bless {}, shift;
- my ($tm_start, $tm_user, $tm_sys) = (time, times);
- #
- # Prevent anything to be written to STDOUT by tieing it to a package
- # that will log anything written there, without letting it go through.
- #
- require CGI::MxScreen::Tie::Stdout;
- tie *main::STDOUT, "CGI::MxScreen::Tie::Stdout";
- #
- # Argument parsing.
- #
- (
- $self->{_screen_list},
- $self->{_initial_state},
- $self->{_cgi_version},
- $self->{_valid_time},
- $self->{_bgcolor},
- $self->{_layout},
- ) =
- cxgetargs(@_,
- -screens => 'HASH',
- -initial => undef,
- -version => [undef, '1.0'],
- -timeout => ['i'],
- -bgcolor => [undef, '#bfbfbf'],
- -layout => ['CGI::MxScreen::Layout'],
- );
- $self->{_start_times} = [$tm_start, $tm_user, $tm_sys];
- $self->{_last_times} = [$tm_start, $tm_user, $tm_sys];
- $self->{_layout} = CGI::MxScreen::Layout->make()
- unless defined $self->{_layout};
- #
- # Perform default initialization if not already done via a call
- # to "use CGI::MxScreen::Config;".
- #
- require CGI::MxScreen::Config;
- CGI::MxScreen::Config::configure(); # Will return if already done
- if (defined $CGI::MxScreen::Config::LOG) {
- $self->{_log} = $CGI::MxScreen::Config::LOG;
- } else {
- use File::Spec;
- require Log::Agent::Logger;
- require Log::Agent::Channel::File;
- my $devnull = Log::Agent::Channel::File->make(
- -filename => File::Spec->devnull,
- -no_prefixing => 1,
- -no_ucfirst => 1,
- -no_newline => 1,
- );
- $self->{_log} = Log::Agent::Logger->make(-channel => $devnull);
- }
- #
- # Now that logging is up, validate the creation routine parameters.
- #
- logcroak "-initial must be either a plain scalar or an ARRAY ref"
- if ref $self->initial_state && ref $self->initial_state ne "ARRAY";
- my $state_name = ref $self->initial_state ?
- $self->initial_state->[0] : $self->initial_state;
- logcroak "initial state '$state_name' is not a known state"
- unless $self->is_valid_state($state_name);
- #
- # Initialize whole script context.
- #
- $self->trace_incoming; # XXX if logtrace_at("info") or logdebug_at("warn")
- my $session = $self->{_session} = CGI::MxScreen::Session->make(
- -serializer => $CGI::MxScreen::Config::SERIALIZER,
- -medium => $CGI::MxScreen::Config::MEDIUM,
- );
- $self->{_context} = $session->restore;
- $self->check_validity();
- #
- # Relink all serialized screens to the new manager.
- #
- my $ctx = $self->ctx;
- if (exists $ctx->{'screens'}) {
- foreach my $screen (values %{$ctx->{'screens'}}) {
- $screen->relink_to_manager($self);
- }
- }
- #
- # For session logging, maintain the following parameters in
- # the private CGI::MxScreen context.
- #
- # log_session unique session ID for logging (IP number-PID)
- # log_starttime time when session started
- # log_cnt counter incremented each time we're invoked
- #
- #
- unless (exists $ctx->{'log_session'}) {
- $ctx->{'log_session'} = CGI::remote_host() . "-" . $$;
- $ctx->{'log_starttime'} = int(time);
- $ctx->{'log_cnt'} = 0;
- } else {
- $ctx->{'log_cnt'}++;
- }
- #
- # From now on, all Log::Agent messages will bear the session ID.
- #
- require Log::Agent::Tag::String;
- use Log::Agent qw(logtags);
- my $tag = Log::Agent::Tag::String->make(
- -name => "session id",
- -value => "(" . $ctx->{'log_session'} . ")",
- );
- my $log = $self->log;
- $log->tags->append($tag);
- logtags()->append($tag);
- $log->warning("");
- $log->warning(\&log_session, $self);
- $log->info(\&log_agent, $self);
- $log->debug(\&log_inc_times, $self, "context restore + log init");
- #
- # Process incoming parameters, trap all errors.
- #
- # Since we might be using CGI::Carp, we must cancel any trap hook by
- # localizing the __DIE__ and __WARN__ special handlers.
- #
- eval {
- local $SIG{__DIE__};
- local $SIG{__WARN__};
- $self->process_params;
- };
- $log->debug(\&log_inc_times, $self, "parameter init");
- $self->internal_error($@) if chomp $@;
- push(@managers, $self);
- return DVAL $self;
- }
- #
- # ->process_params
- #
- # Get CGI parameters, fill internal data structures.
- #
- sub process_params {
- DFEATURE my $f_;
- my $self = shift;
- #
- # Save params provided by CGI
- #
- # It is a quite big story because there are different possiblities
- # for the store location according to the way the fields have been
- # recorded, and also according to the storage indication settings.
- #
- # When the field has been recorded at display time (use of
- # record_field method), it might contain some storage indications
- # (see Form::Field). It may also contain an indication to not save
- # the value (useful for password). Anyway, when the incoming param
- # matches a recorded field from the last display, the store_value
- # method of the field is invoked to perform the task according to
- # the indication. Returning true indicates there is no need to
- # save the param value in the MxScreen repository (see
- # below). Actually, either the value has been save somewhere else
- # or there were some indication to not keep the value persistent.
- #
- # When there is no specific indication for the storage (either the
- # field has not been recorded but has been merely displayed, or
- # the store_value method returned false), the value is memorized
- # into the MxScreen repository for the orphan params. It is a
- # dedicated section of the context. Each orphan params is stored
- # in that section under the index of the screen name. All the orphan
- # params are replayed --put into the CGI param list-- to benefit
- # from their values when the field is once again displayed.
- #
- # NOTES: You have to know that a button press returns also a value
- # into the incoming CGI param list. The following code needs to
- # take care of that by filtering them before being considered as
- # orphan fields. For simple button, it is quite easy since the
- # param name must have been recorded into the Mxscreen Button
- # list, but for image button the returned param does not match
- # exactly the one recorded. For this latter button, the returned
- # param is in fact 2 params which indicates the click
- # location. Their name is composed by the param name (recorded in
- # the Mxscreen Button list) plus '.x' or '.y'.
- #
- # NOTES: CGI param list does not alway returned a value for all
- # displayed field of the screen. For some specific elements (for
- # instance checkbox group), no value is returned when the field is
- # cleared (no box checked in the previous example). This clear
- # value must however be saved into the storage location. To cope
- # this problem, all the known displayed fields (those in the
- # recorded list of fields, and those in the orphan repository of
- # the screen) are checked to validate the existence of a value
- # into the CGI param list. When no value is found, a clear value
- # ('') is enforced.
- #
- # load the package of the last screen where all needed classes
- # should have been defined.
- my $current_state = $self->initial_state;
- $current_state = $self->ctx->{'current_state'} if
- (defined $self->ctx->{'current_state'});
- my ($screen_name) = $self->scatter($current_state);
- $self->load_screen_package($screen_name);
- # build a easy access way to recorded field and button: make a
- # hash from the array
- my $var_ctx = $self->context(PERSISTENT);
- my $field_hash = {};
- for my $field (@{$self->context(SCREEN_FIELD)}) {
- DASSERT $field->isa('CGI::MxScreen::Form::Field');
- $field_hash->{$field->name} = $field;
- }
- my $button_hash = {};
- for my $button (@{$self->context(SCREEN_BUTTON)}) {
- DASSERT $button->isa('CGI::MxScreen::Form::Button');
- $button_hash->{$button->name} = $button;
- }
- #
- # Patch the CGI param list for fields which are known to be
- # displayed but no value appears in the CGI list.
- #
- my $cgi_param = $self->context(CGI_PARAM);
- while (my ($k, $v) = each %{$cgi_param->{$screen_name}}) {
- CGI::param(-name => $k, -values => $v) unless
- defined CGI::param($k);
- }
- while (my ($k, $v) = each %$field_hash) {
- CGI::param(-name => $k, -values => $v->value) unless
- defined CGI::param($k);
- }
- # walkthrough the CGI param list to store values
- for my $param (CGI::param()) {
- DTRACE "storing incoming param $param";
- my $field = $field_hash->{$param};
- # return form CGI param might be either a single element or a
- # list of elements. To get all of them, an array context must
- # be used. Then, the value that will be stored is either the
- # array reference or the first and single element of the
- # array.
- my @value = CGI::param($param);
- my $value = $#value == 0 ? $value[0]: \@value;
- if (defined $field) {
- #
- # Patch the value (if needed)
- # Then store value according to the storage indication given in
- # the field (if any)
- #
- my ($patched, $nvalue) = $field->patch_value($value);
- if ($patched) {
- CGI::param(-name => $param, -values => $nvalue) if $patched;
- next if $field->store_value($var_ctx, $nvalue);
- } else {
- next if $field->store_value($var_ctx, $value);
- }
- }
- # no storage indication is present
- #
- # perhaps it was a button rather than a field
- #
- # image button press is embarrassing. In such a case, the
- # returned param is not 1 but 2 params which represents the
- # location of the click within the image
- if ($param =~ /(.*)\.([xy])$/) {
- if (defined (my $x = CGI::param("$1.x")) &&
- defined (my $y = CGI::param("$1.y"))) {
- next if $2 eq "y"; # do the job only for x
- $param = $1;
- }
- }
- if (defined $button_hash->{$param}) {
- $self->internal_error(
- "invalid input form: buttons '" . $self->button_pressed->name .
- "' and '$param' were simultaneously pressed!")
- if defined $self->button_pressed;
- # Remember it as the button that was pressed
- $self->{_button_pressed} = $button_hash->{$param};
- next;
- }
- # It is an orphan field that has not been saved. Keep it in
- # mind into the param repository. The param context is stored
- # under the name of the screen to build a kind of
- # hierachy. That allows the clean up functionality when
- # leaving a screen (on explicit request).
- $cgi_param->{$screen_name}->{$param} = $value;
- }
- # all orphan params will populate the CGI's param list if they are
- # not already present. That will allow to prefill fields when
- # redisplay and to give an access to their values with regular
- # CGI::param().
- #
- # Information is organized in a hash table where the key is the
- # screen id (state name) and the value is another hash. The latter
- # contains the pair of data: symbol, value that must be restored.
- while (my ($screen , $hash) = each %$cgi_param) {
- while (my ($k, $v) = each %$hash) {
- CGI::param(-name => $k, -values => $v);
- }
- }
- return DVOID;
- }
- #########################################################################
- # Internal Attribute Access: these methods are not intended to be used #
- # from the external world. #
- #########################################################################
- sub screen_list { $_[0]->{'_screen_list'} }
- sub context_root { $_[0]->{'_context'} }
- sub screen { $_[0]->{'_screen'} }
- sub session { $_[0]->{'_session'} }
- sub cgi_version { $_[0]->{'_cgi_version'} }
- sub valid_time { $_[0]->{'_valid_time'} }
- sub initial_state { $_[0]->{'_initial_state'} }
- sub bgcolor { $_[0]->{'_bgcolor'} }
- sub layout { $_[0]->{'_layout'} }
- sub log { $_[0]->{'_log'} }
- sub start_times { $_[0]->{'_start_times'} }
- sub last_times { $_[0]->{'_last_times'} }
- sub button_pressed { $_[0]->{_button_pressed} }
- sub ctx {
- defined $_[0]->{'_context'} ? $_[0]->{'_context'}->[MXSCREEN] : {}
- }
- #
- # ->is_valid_state
- #
- # Check whether state is known
- #
- sub is_valid_state {
- DFEATURE my $f_;
- my $self = shift;
- my ($state) = @_;
- return DVAL exists $self->screen_list->{$state};
- }
- #
- # ->load_screen_package
- #
- # Load source file for the class implementing the screen $name, unless
- # it is already present.
- #
- sub load_screen_package {
- DFEATURE my $f_;
- my $self = shift;
- my ($name) = @_;
- DREQUIRE $self->is_valid_state($name), "valid state '$name'";
- my ($class_name) = cgetargs(@{$self->screen_list->{$name}},
- {-strict => 0},
- qw(class));
- #
- # The following eval "" attempts to load the screen class by using
- # a require, assuming there is one class by file. However, we
- # check for the presence of an @ISA variable in the target package
- # before performing the require, since the application could have
- # already loaded all the screen classes. Given that all screens must
- # inherit from CGI::MxScreen::Screen, we know @ISA is defined if the
- # package is present.
- #
- eval "require $class_name unless defined \@${class_name}::ISA;";
- if (chomp $@) {
- logerr "loading of $class_name failed: $@";
- logdie "can't locate class \"$class_name\" for screen state \"$name\"";
- }
- return DVOID;
- }
- #
- # ->make_screen
- #
- # Create the screen for given state.
- #
- sub make_screen {
- DFEATURE my $f_;
- my $self = shift;
- my ($name) = @_;
- DREQUIRE $self->is_valid_state($name), "valid state '$name'";
- $self->load_screen_package($name);
- my ($class_name, @remaining) = cgetargs(@{$self->screen_list->{$name}},
- {-strict => 0, -extra => 1},
- qw(class));
- #
- # If the state has already been seen already, it has been serialized
- # in the context, but it needs to be relinked to the new manager instance.
- #
- # Otherwise, a new object is created and remembered in the context.
- #
- my $cxt = $self->ctx; # CGI::MxScreen own private context
- my $screen;
- if (exists $cxt->{'screens'}->{$name}) {
- $screen = $cxt->{'screens'}->{$name};
- $screen->remake($self);
- } else {
- $screen = $class_name->make(
- -manager => $self,
- -name => $name,
- @remaining
- );
- $cxt->{'screens'}->{$name} = $screen;
- }
- return DVAL $screen;
- }
- #
- # ->scatter
- #
- # Return:
- # either a list with a single element when incoming param is a
- # scalar value or a list with all element of the incoming list.
- #
- sub scatter {
- DFEATURE my $f_;
- my $self = shift;
- my ($id) = @_;
- return DARY @$id if ref $id eq 'ARRAY';
- return DARY ($id);
- }
- #
- # ->obj_scatter
- #
- # Same as scatter(), but handles ($obj, $routine, @args) as well.
- # Supplies the screen if no blessed object is identified in the first
- # position of the list.
- #
- sub obj_scatter {
- DFEATURE my $f_;
- my $self = shift;
- my ($screen, $id) = @_;
- return DARY ($screen, $id) unless ref $id eq 'ARRAY';
- if (ref $id->[0] && UNIVERSAL::isa($id->[0], "UNIVERSAL")) {
- $screen = $id->[0];
- return DARY ($screen, @$id[1..$#$id]);
- }
- return DARY ($screen, @$id);
- }
- #########################################################################
- # Class Feature: usable from the external world #
- #########################################################################
- #
- # ->context
- #
- # return a reference of a given section withtin the overal context
- # area
- #
- # Arguments:
- # $index: index of the context section to returned
- #
- # Return:
- # a reference to the requested context section
- #
- sub context {
- DFEATURE my $f_;
- DREQUIRE $_[1] =~ /^\d+$/;
- DREQUIRE $_[1] >= 0 && $_[1] < CONTEXT_COUNT;
- return DVAL $_[0]->context_root->[$_[1]];
- }
- #
- # ->spring_screen
- # ->previous_screen
- # ->current_screen
- #
- # Returns [state, display_args]
- #
- sub spring_screen {
- DFEATURE my $f_;
- return DVAL $_[0]->ctx->{'spring_state'}; # Last stable state(args)
- }
- sub previous_screen {
- DFEATURE my $f_;
- return DVAL $_[0]->ctx->{'previous_state'}; # Previous state(args)
- }
- sub current_screen {
- DFEATURE my $f_;
- return DVAL $_[0]->ctx->{'current_state'}; # Current state(args)
- }
- #
- # ->play
- #
- # Play the sequence of action necessary to display the new screen.
- #
- sub play {
- DFEATURE my $f_;
- my $self = shift;
- # coderef is a temporary arg until storable is able to select things to
- # store (storable::Hook)
- my ($coderef) = @_;
- my $log = $self->log;
- $log->debug(\&log_inc_times, $self, "outside CGI::MxScreen");
- #
- # Compute target screen, trap all errors.
- #
- # Since we might be using CGI::Carp, we must cancel any trap hook by
- # localizing the __DIE__ and __WARN__ special handlers.
- #
- my ($screen, $args);
- eval {
- local $SIG{__DIE__};
- local $SIG{__WARN__};
- ($screen, $args) = $self->compute_screen;
- };
- $log->debug(\&log_inc_times, $self, "screen computation");
- $self->internal_error($@) if chomp $@;
- #
- # Emit CGI headers
- # From now on, output is safe and will not get us a server error.
- #
- untie *main::STDOUT; # Restore original STDOUT stream
- #
- # If they configured us to buffer all STDOUT until context is ready
- # to be emitted, then create object, print headers and mark the
- # output of headers as done: further output to STDOUT will be buffered
- # and printed only after the context.
- #
- # The reason for this is to have the context emitted before any other
- # form widget. That way, pressing a submit button before the whole form
- # is loaded in the browser won't matter as much, since we'll have at
- # least the context to propagate in the POST parameters.
- #
- my $stdout;
- if ($CGI::MxScreen::cf::mx_buffer_stdout) {
- require CGI::MxScreen::Tie::Buffered_Output;
- $stdout = tie *main::STDOUT, "CGI::MxScreen::Tie::Buffered_Output";
- }
- #
- # Display screen, with proper "bounce" exception support.
- # Returns screen that was finally displayed.
- #
- $screen = $self->display($screen, $args, $stdout);
- $log->debug(\&log_inc_times, $self, "\"%s\" display", $screen->name);
- #
- # Snapshot current time and last modification date of the
- # scriptright before saving context. That fields can be used to
- # check for session validity.
- #
- $self->ctx->{'time'} = time;
- $self->ctx->{'script_date'} = (stat($0))[9];
- #
- # Cleanup context to avoid saving transient data
- #
- &{$coderef}() if defined $coderef; # TBR
- for my $f (@{$self->context_root->[SCREEN_FIELD]}) {
- DASSERT $f->isa('CGI::MxScreen::Form::Field');
- $f->cleanup();
- }
- for my $b (@{$self->context_root->[SCREEN_BUTTON]}) {
- DASSERT $b->isa('CGI::MxScreen::Form::Button');
- $b->cleanup();
- }
- #
- # If STDOUT was bufferd, the context must be emitted explicitely
- # between the header of the form and the remaining data.
- #
- if (defined $stdout) {
- my $context = $self->session->save;
- $stdout->print_all($context);
- untie_stdout();
- } else {
- print $self->session->save;
- }
- $log->debug(\&log_inc_times, $self, "context save");
- #
- # Emit CGI trailers.
- #
- print CGI::endform;
- my $layout = $self->layout;
- $layout->postamble;
- $layout->end_HTML;
- return DVOID;
- }
- #
- # ->compute_screen
- #
- # Compute target screen, and run and enter/leave hooks if we change screens.
- # This routine does not display anything, but runs all the action callbacks.
- #
- # Returns new screen object, and a ref to the argument list.
- #
- sub compute_screen {
- DFEATURE my $f_;
- my $self = shift;
- my ($current_state, $previous_state, $new_state);
- my ($origin_name, $target_name, @arg_list);
- my $screen;
- my $errors = 0;
- my $ctx = $self->ctx;
- # get the current state from the context its format can be either
- # 'screen_name' or ['screen_name', @arg_list]. 'screen_name' is the
- # symbol key given to a screen name into the given screen list (at
- # make time) and @arg_list is a list of arg to pass to the display
- # routine of the screen.
- $current_state = $self->initial_state;
- $current_state = $ctx->{'current_state'} if
- (defined $ctx->{'current_state'});
- $previous_state = $current_state;
- $new_state = $current_state;
- #
- # Compute the destination and process the associated actions when
- # a button has been detected as pressed (during the make method).
- #
- # If we could not identify a button that was pressed, we'll simply
- # remain in the current state and re-display the form unless there was
- # a default button recorded in the previous screen.
- #
- my $button_pressed = $self->button_pressed;
- if ($ctx->{'log_cnt'} && !defined $button_pressed) {
- #
- # Create the previous screen to lookup for a default button
- #
- ($origin_name) = $self->scatter($previous_state);
- $screen = $self->make_screen($origin_name);
- my $default = $screen->default_button;
- if (defined $default) {
- $button_pressed = $self->{_button_pressed} = $default;
- $self->log->warning("no button pressed, using default \"%s\"",
- $default->value);
- } else {
- $self->log->error(
- "no button pressed, no default, will stay in same state");
- }
- }
- if (defined $button_pressed) {
- #
- # Create the previous screen to perform the actions
- # Screen could have been created above, during the default
- # button computation, hence the check.
- #
- unless (defined $screen) {
- ($origin_name) = $self->scatter($previous_state);
- $screen = $self->make_screen($origin_name);
- }
- # Those are not serialized
- DASSERT !defined $screen->error_env, "no callback error condition";
- DASSERT !defined $screen->error, "no user error condition";
- my $act_env; # Action environment
- if (defined $button_pressed->action) {
- DASSERT ref $button_pressed->action eq 'ARRAY';
- use CGI::MxScreen::Error qw(is_mx_errcode);
- require CGI::MxScreen::Action_Env;
- $act_env = CGI::MxScreen::Action_Env->make();
- for my $action (@{$button_pressed->action}) {
- my ($obj, $routine, @routine_arg) =
- $self->obj_scatter($screen, $action);
- my $errcode = $obj->$routine(@routine_arg, $act_env);
- #
- # Temporary safety net whilst migration of all callback
- # returned values is ongoing.
- #
- if ($errcode == 0 || $errcode == 1) {
- logwarn "callback %s->%s returned OLD boolean status",
- ref $obj, $routine;
- $errcode = $errcode ? CGI_MX_OK : CGI_MX_ABORT;
- }
- VERIFY is_mx_errcode($errcode),
- "callback ", ref($obj), "->$routine returns valid code",
- " -- returned $errcode";
- next if $errcode == CGI_MX_OK;
- #
- # an error occurred, don't process the remaining
- # of actions if it is CGI_MX_ABORT.
- #
- # The screen is tagged with an error flag and the state
- # destination is resumed to the origin screen.
- #
- my $called = sprintf "%s->%s", ref($obj), $routine;
- my $binfo = sprintf "for button \"%s\"",
- $button_pressed->value;
- $binfo .= sprintf " (%s)", $button_pressed->name
- if $button_pressed->name ne $button_pressed->value;
- DTRACE "error in action callback: $called $binfo";
- $self->log->error("action callback $called failed $binfo%s",
- $errcode == CGI_MX_ABORT ? ", aborting" : "");
- $errors++;
- $screen->set_error_env($act_env);
- $act_env->add_error($obj, $routine, \@routine_arg);
- last if $errcode == CGI_MX_ABORT;
- }
- $new_state = $current_state if $errors;
- }
- #
- # Get the destination
- #
- # * when an error was found, we look at -on_error or -dyn_on_error,
- # and if one is found, we clear the error condition.
- # * when no error is raised, we look at -dyn_target or -target.
- #
- if ($errors) {
- #
- # Look for possible error trapping, which will force a move to
- # an alternate screen. The error condition is reset, therefore
- # the internal context of the screen will be cleared.
- #
- # For -dyn_on_error, we append the action environment.
- #
- DASSERT defined $act_env, "at least one action ran";
- if ($button_pressed->has_error_trap) {
- my $dyn = $button_pressed->dyn_on_error;
- if (defined $dyn) {
- my ($routine, @args) = $self->scatter($dyn);
- DASSERT $screen->can($routine);
- $new_state = $screen->$routine(@args, $act_env);
- } else {
- $new_state = $button_pressed->on_error;
- }
- DASSERT defined $new_state;
- $errors = 0; # Moving to alternate screen
- }
- } else {
- #
- # No error found.
- #
- if ($button_pressed->is_computed_target) {
- my ($routine, @args) =
- $self->scatter($button_pressed->dyn_target);
- DASSERT $screen->can($routine);
- $new_state = $screen->$routine(@args);
- }
- else {
- $new_state = $button_pressed->target;
- }
- }
- }
- # clear context area dedicated to save field handles
- $self->context_root->[SCREEN_FIELD] = [];
- $self->context_root->[SCREEN_BUTTON] = [];
- # context might have been saved by the screen -> also clear the copy
- $screen->_clear_internal_context() if defined $screen && !$errors;
- #
- # update the MXSCREEN context
- #
- $ctx->{'current_state'} = $new_state;
- $ctx->{'previous_state'} = $previous_state unless $errors;
- $ctx->{'cgi_version'} = $self->cgi_version;
- $ctx->{'bin_version'} = $BIN_VERSION;
- $self->log->notice(\&log_state, $self, $previous_state, $new_state);
- #
- # Create the destination state (if needed)
- # Then call ->leave and ->enter hooks.
- #
- ($target_name, @arg_list) = $self->scatter($new_state);
- unless (defined $screen && $target_name eq $origin_name) {
- my $prev_screen = $screen;
- $screen = $self->make_screen($target_name);
- if (defined $prev_screen) {
- $prev_screen->leave($screen);
- $ctx->{'spring_state'} = $previous_state; # Where we came from
- }
- $screen->enter($prev_screen);
- }
- return DARY ($screen, \@arg_list);
- }
- #
- # ->display
- #
- # Display $screen, with args @$args, with proper support for screen "bounce".
- #
- # If $stdout is not undef, then it is a ref to a tied object, meaning STDOUT
- # is buffered. When bouncing with untied STDOUT, the layout and the headers
- # can only be emitted once, i.e. for the original screen. A warning is issued
- # if bouncing.
- #
- # Returns screen that was finally displayed.
- #
- sub display {
- DFEATURE my $f_;
- my $self = shift;
- my ($screen, $args, $stdout) = @_;
- for (my $i = 0; $i < 20; $i++) { # Max 20 bounces
- #
- # Can only emit the layout and the header each time when $stdout
- # is tied. We always emit the first time, naturally, since we
- # don't know whether we'll bounce at all.
- #
- if ($i == 0 || defined $stdout) {
- #
- # The layout object controls the following aspects:
- #
- # html headers
- # preamble
- # <form goes here>
- # postabmle
- # html trailers
- #
- my $layout = $self->layout;
- $layout->init($screen);
- $layout->start_HTML(
- -title => $screen->screen_title,
- -bgcolor => $screen->bgcolor,
- );
- $layout->preamble;
- #
- # Start the form
- #
- my @args = (-method => 'POST', -action => CGI::url());
- print $CGI::DISABLE_UPLOADS ?
- CGI::startform(@args) : CGI::start_multipart_form(@args);
- }
- $stdout->header_ok if defined $stdout; # Buffer remaining as BODY
- #
- # Display target screen, trap all errors.
- #
- eval {
- local $SIG{__DIE__};
- local $SIG{__WARN__};
- $screen->display(@$args);
- };
- #
- # Deal with "bounce" exceptions.
- #
- if (ref $@ && $@->isa("CGI::MxScreen::Exception::Bounce")) {
- my $old_state = $self->current_screen;
- my $new_state = $@->target;
- my $log = $self->log;
- my $old_name = $screen->name;
- my $old_screen = $screen;
- $log->notice(\&log_bounce, $self, $old_state, $new_state, $@);
- $log->debug(\&log_inc_times, $self, "bounce on \"%s\"", $old_name);
- my ($target_name, @arg_list) = $self->scatter($new_state);
- #
- # Clear buffered data in tied STDOUT, so we may start afresh
- # with new screen. If the old screen had started emitting data
- # before bouncing, warn them: the screen should not have done so
- # anyway, so we may discard data bluntly.
- #
- if (defined $stdout) {
- my $discarded = $stdout->reset;
- logwarn "discarded %d byte%s emitted by \"%s\" " .
- "(before bouncing to \"%s\")",
- $discarded, $discarded == 1 ? "" : "s",
- $old_name, $target_name if $discarded;
- }
- #
- # Set args for next loop.
- #
- $screen = $self->make_screen($target_name);
- $args = \@arg_list;
- $self->ctx->{'current_state'} = $new_state;
- #
- # Need to call ->leave() and ->enter() when states are different.
- # We pass undef to leave() to indicate that we left as the result
- # of a bounce.
- #
- # We don't alter `spring_state' though.
- #
- if ($target_name ne $screen->name) {
- $old_screen->leave(undef); # Signals: bounced
- $screen->enter($old_screen);
- }
- next; # Restart display loop
- }
- #
- # Regular display error.
- #
- if (ref $@ || chomp $@) {
- my $msg = $@;
- $msg =~ s/^\(.*?\)\s+//; # Remove already added session tag
- $self->log->critical("display error for screen \"%s\": %s",
- $screen->name, $msg);
- #
- # If they buffered STDOUT, it's nice, because the screen will not
- # mix regular output and the error message. And since we discard
- # even the form header, the Content-Type printed by CGI::Carp will
- # not even show!
- #
- untie_stdout(1) if defined $stdout;
- logdie $msg;
- }
- return DVAL $screen; # Successfully displayed the screen
- }
- $self->log->critical("too many screen bounces");
- logdie "possible infinite loop detected, aborting";
- }
- #
- # ->check_validity
- #
- # Check context validity: proper version, no timeout.
- #
- sub check_validity {
- DFEATURE my $f_;
- my $self = shift;
- unless (defined $self->context_root) {
- logerr "mangled context from %s", CGI::remote_host();
- $self->internal_error("cannot retrieve application context");
- }
- my $ctx = $self->ctx;
- return DVOID unless exists $ctx->{'cgi_version'}; # Empty context
- #
- # Ensure binary version (which traces variations in the way session
- # context are represented) is compatible.
- #
- my $bin = $ctx->{'bin_version'};
- if ($bin > $BIN_VERSION) {
- $self->internal_error(<<EOS);
- Script session used a format (v$bin) more recent than I am (v$BIN_VERSION).
- Please restart a new session.
- EOS
- }
- #
- # check that the script file has not been modified (compare the
- # last modification time on the file system)
- #
- if ($ctx->{'script_date'} != (stat($0))[9]) {
- $self->internal_error(<<EOS);
- Script file has been modified since the last display,
- please restart a new session.
- EOS
- }
- #
- # check whether the cgi version is still the same
- #
- if (defined $ctx->{'cgi_version'}) {
- my $version = $ctx->{'cgi_version'};
- if ($version ne $self->cgi_version) {
- $self->internal_error(<<EOS);
- Script version has evolved since the last display, please restart a new session.
- EOS
- }
- }
- #
- # check whether the timeout is not exhausted
- #
- if (defined $self->valid_time && defined $ctx->{'time'}) {
- my $last_time = $ctx->{'time'};
- if ((time - $last_time) > $self->valid_time) {
- $self->internal_error(<<EOS);
- Session timeout since the last display, please restart a new session.
- EOS
- }
- }
- return DVOID;
- }
- #
- # ->internal_error
- #
- #
- sub internal_error {
- DFEATURE my $f_;
- my $self = shift;
- my ($message) = @_;
- my $logmsg = $message;
- $logmsg =~ s/\s+/ /sg;
- logerr "internal error: $logmsg";
- untie_stdout(1); # Restore original STDOUT stream, discard all
- my $layout = $self->layout;
- $layout->init(undef);
- $layout->start_HTML("Internal Script Error");
- $layout->preamble;
- print CGI::h1("Internal Script Error");
- print CGI::p(CGI::tt(ucfirst($message)));
- print CGI::p(CGI::a({-href => CGI::url()}, "Restart a new session"));
- $layout->postamble;
- $layout->end_HTML;
- my $log = $self->log;
- $log->alert("internal error: $logmsg") if defined $log;
- exit 0;
- }
- #
- # ->trace_incoming
- #
- # Trace incoming parameters
- #
- sub trace_incoming {
- DFEATURE my $f_;
- foreach my $p (CGI::param()) {
- my $value = CGI::param($p);
- DTRACE(TRC_INFO, "incoming param: '$p' => '$value'");
- }
- return DVOID;
- }
- #
- # (log_session) -- logging callback
- #
- # Log session state
- #
- sub log_session {
- DFEATURE my $f_;
- my $self = shift;
- my $current = $self->current_screen || $self->initial_state;
- my $cxt = $self->ctx;
- my $cnt = $cxt->{log_cnt};
- my ($state) = $self->scatter($current);
- my $user = CGI::remote_user();
- my @url_param = CGI::url_param();
- my $query = join(';', map { "$_=" . CGI::url_param($_) } @url_param);
- my $msg = sprintf "[%s/%d]", $state, $cnt;
- $msg .= sprintf " t=%s", relative_age(int(time) - $cxt->{log_starttime});
- $msg .= sprintf " d=%s", relative_age($^T - $cxt->{time}) if $cnt;
- $msg .= " u=\"$user\"" if $user;
- #
- # If there were no parameters on the URL, CGI still returns one entry
- # for a "keywords" parameter, so we need to guard against this as well.
- #
- $msg .= " q=\"$query\"" if $query ne '' && $query ne 'keywords=';
- return DVAL $msg;
- }
- #
- # (log_state) -- logging callback
- #
- # Log state change and button pressed
- #
- sub log_state {
- DFEATURE my $f_;
- my $self = shift;
- my ($old, $new) = @_;
- my $cxt = $self->ctx;
- my $cnt = $cxt->{log_cnt};
- my ($old_state, @old_args) = $self->scatter($old);
- my $msg = sprintf "%s%s",
- $old_state, @old_args ? ("(" . join(', ', @old_args) . ")") : "";
- unless ($cnt) { # First time
- return DVAL '' unless @old_args; # Don't log state if no args
- return DVAL $msg
- }
- my ($new_state, @new_args) = $self->scatter($new);
- $msg .= sprintf " -> %s%s",
- $new_state, @new_args ? ("(" . join(', ', @new_args) . ")") : "";
- #
- # Log button pressed, or bounce indication.
- #
- my $button = $self->button_pressed;
- if (defined $button) {
- my $name = $button->name;
- my $value = $button->value;
- $msg .= sprintf " on \"%s\" pressed", $value;
- $msg .= sprintf " (%s)", $name if $value ne $name;
- }
- return DVAL $msg;
- }
- #
- # (log_bounce) -- logging callback
- #
- # Log screen bounces
- #
- sub log_bounce {
- DFEATURE my $f_;
- my $self = shift;
- my ($old, $new, $bounce) = @_;
- my $cxt = $self->ctx;
- my ($old_state, @old_args) = $self->scatter($old);
- my $msg = sprintf "%s%s",
- $old_state, @old_args ? ("(" . join(', ', @old_args) . ")") : "";
- my ($new_state, @new_args) = $self->scatter($new);
- $msg .= sprintf " -> %s%s",
- $new_state, @new_args ? ("(" . join(', ', @new_args) . ")") : "";
- $msg .= " (via $bounce)";
- return DVAL $msg;
- }
- #
- # (log_agent) -- logging callback
- #
- # Log user agent
- #
- sub log_agent {
- DFEATURE my $f_;
- my $self = shift;
- my $cnt = $self->ctx->{log_cnt};
- return if $cnt; # Nothing after first time
- return DVAL sprintf "using \"%s\"", CGI::user_agent();
- }
- #
- # (log_inc_times) -- logging callback
- #
- # Log incremental time between values recorded in last_times and now.
- # Update last_times as a side effect for next incremental logging.
- #
- sub log_inc_times {
- DFEATURE my $f_;
- my $self = shift;
- my ($fmt, @args) = @_; # Can be single string or (fmt, args)
- $fmt = sprintf $fmt, @args if @args;
- my $times = $self->last_times;
- my $new_times = [time, (times)[0,1]];
- $self->{_last_times} = $new_times;
- my @delta;
- for (my $i = 0; $i < @$times; $i++) {
- $delta[$i] = $new_times->[$i] - $times->[$i];
- }
- return DVAL sprintf "t=%.2fs usr=%.2fs sys=%.2fs [%s]", @delta, $fmt;
- }
- #
- # (log_total_time) -- logging callback
- #
- # Log total time spent since start_times.
- #
- sub log_total_time {
- DFEATURE my $f_;
- my $self = shift;
- my $times = $self->start_times;
- my $new_times = [time, (times)[0,1]];
- my @delta;
- for (my $i = 0; $i < @$times; $i++) {
- $delta[$i] = $new_times->[$i] - $times->[$i];
- }
- my $runtime = time - $^T;
- return DVAL sprintf "t=%.2fs usr=%.2fs sys=%.2fs [total time] T=%.2fs",
- @delta, $runtime;
- }
- #
- # relative_age
- #
- # Given seconds, convert to 4d9h23m15s format.
- #
- sub relative_age {
- DFEATURE my $f_;
- my ($secs) = @_;
- my ($days, $hours, $mins);
- $days = int($secs / (24 * 60 * 60));
- $secs -= $days * (24 * 60 * 60);
- $hours = int($secs / (60 * 60));
- $secs -= $hours * (60 * 60);
- $mins = int($secs / 60);
- $secs -= $mins * 60;
- my $retstr = '';
- $retstr .= $days . "d" if $days;
- $retstr .= $hours . "h" if $hours;
- $retstr .= $mins . "m" if $mins;
- $retstr .= int($secs + 0.5) . "s"; # can be fractional with Time::HiRes
- return DVAL $retstr;
- }
- #
- # ::add_utils_path -- static
- #
- # Screen designers can identify new Form::Utils packages for their own
- # specific uses with this routine. It must be invoked in the user
- # script as a static routine => CGI::MxScreen::add_utils_path , and
- # before the creation of the MxScreen object.
- #
- # NB: This routine name is misleading: it does not involve file paths, but
- # module names. The purpose is to allow some kind of routine lookup to
- # be able to locate a validation routine named "is_time" for instance.
- # I'm keeping it for now, because it's been used in production, but this
- # mechanism will have to be revisited.
- # -- RAM, 13/04/2001
- #
- sub add_utils_path {
- DFEATURE my $f_;
- VERIFY defined($_[0]) && !UNIVERSAL::isa($_[0], __PACKAGE__);
- require CGI::MxScreen::Form::Utils;
- CGI::MxScreen::Form::Utils::add_path(@_);
- return DVOID;
- }
- #
- # ::untie_stdout
- #
- # Safely untie STDOUT by forcing a DESTROY, in case someone holds a reference
- # on the tied object.
- #
- sub untie_stdout {
- DFEATURE my $f_;
- my ($discard) = @_;
- my $stdout = tied *main::STDOUT;
- #
- # Within CGI::MxScreen, all the packages that can be tied to STDOUT are
- # heirs of CGI::MxScreen::Tie::Sinkable, which provides a discard_all()
- # method.
- #
- DASSERT !defined($stdout) || $stdout->isa("CGI::MxScreen::Tie::Sinkable");
- if (defined $stdout) {
- logtrc 'info', "un-tieing STDOUT (%s) with%s discarding",
- ref $stdout, $discard ? "" : "out";
- $stdout->discard_all if defined $discard && $discard;
- $stdout->DESTROY;
- untie *main::STDOUT;
- }
- return DVOID;
- }
- #
- # END
- #
- # Whatever happens, log total running time, provided they created a manager.
- #
- sub END {
- untie_stdout(); # They might have not got a chance to do so yet
- #
- # Log running time, once per manager.
- #
- foreach my $self (@managers) {
- $self->log->info(\&log_total_time, $self);
- }
- }
- 1;
- __END__
- =head1 NAME
- CGI::MxScreen - a multi-screen stateful CGI framework
- =head1 SYNOPSIS
- require CGI::MxScreen;
- my $manager = CGI::MxScreen->make(
- -bgcolor => "#dedeef",
- -screens =>
- {
- "state_1" =>
- [-class => "STATE_1", -title => "Hello"],
- "state_2" =>
- [-class => "STATE_2", -title => "Hello #2"],
- },
- -initial => "state_1",
- -version => "1.0",
- );
- $manager->play();
- =head1 DESCRIPTION
- C<CGI::MxScreen> is a framework for building multi-screen stateful CGI
- programs. It is rather object-oriented, with some peculiarities brought
- by persistency constraints: all objects must be handled by C<Storable>.
- C<CGI::MxScreen> is based on the C<CGI> module, and co-operates with it,
- meaning you are able to use most C<CGI> calls normally. The few places
- where you should not is where C<CGI::MxScreen> supersedes the C<CGI>
- functionalities: for instance, there's no need to propagate hidden values
- when you use C<CGI::MxScreen>.
- C<CGI::MxScreen> is architected around the concept of B<screens>.
- Among the set of defined screens within the same script, only one is visible
- at a time. One moves around the various screens by pressing buttons,
- which submit data to the server and possibly move you to a different screen.
- The state machine is handled by C<CGI::MxScreen>, the user only defines
- which state (I<screen>) a button shall move the application to
- C<CGI::MxScreen> is stateful in the sense that many of the runtime objects
- created to operate (and screens are among those) are made persistent.
- This is a very interesting property, because you do not have to worry
- too much about the underlying stateless nature of the CGI protocol. The
- C<CGI> module brought the statefulness to the level of form controls, but
- C<CGI::MxScreen> raises it to the level of the application itself.
- C<CGI::MxScreen> is not meant to be used for so-called I<quick and dirty>
- scripts, or for scripts which do not require some fair amount of round trips
- between the browser and the server. You'll be better off with using
- the good old C<CGI> module. However, for more complex web applications,
- where there is a fair amount of processing required on the server side, and
- where each script involves several states, C<CGI::MxScreen> is for you.
- OK, enough talking.
- =head1 FRAMEWORK
- This section describes the C<CGI::MxScreen> framework. If you wish to
- read about the interface of the C<CGI::MxScreen> managing object, please
- skip down to L<"INTERFACE">.
- =head2 Features
- Here are the main features of C<CGI::MxScreen>:
- =over 4
- =item *
- The module is a superset of the C<CGI> module. You can continue to use C<CGI>
- routines wherever you like.
- =item *
- It handles B<sessions> for you, saving much of the application state, and
- making CGI hidden parameters useless. You may save sessions within the
- browser, or to files, or you may even build your own medium backend.
- You may also define your own serializing options, although C<Storable> is
- natively supported.
- See L<CGI::MxScreen::Session::Medium> for the medium interface and
- L<CGI::MxScreen::Serializer> for the serialization interface.
- =item *
- It handles the state machine for you. You define the various B<screen
- objects>, and then specify, for each submit button, which state the
- application should go. The target state can be specified statically,
- or computed dynamically by the application. Action routines can be
- attached to the button, to run some processing during the state change.
- See L<CGI::MxScreen::Form::Button> for more information.
- =item *
- It has an object-oriented design. Each screen is an object inheriting from
- C<CGI::MxScreen::Screen> and redefining the C<display> routine, at least.
- There are also C<enter> and C<leave> hooks for each screen.
- Each created screen object is made persistent accross the whole session.
- See L<CGI::MxScreen::Screen> for the full interface.
- =item *
- Any script output done before the screen's C<display> routine is called
- will be trapped and discarded (with logging showing the place where such a
- violation occurs). This architecturally enforces proper application behaviour.
- Furthermore, by default, the whole output is buffered until it is
- time to save the context, thereby protecting against further submits
- with a partially received form on the browser side, and also strengthening
- the protection when the application uses bounce exceptions to jump into
- another state.
- =item *
- Each CGI parameter (form control) can be given an explicit storage indication
- (i.e. how the application should dispose of the value), a validation routine,
- and an on-the-fly patching routines (to normalize values, for instance).
- Each parameter may also be given a mandatory status, causing an error when
- it is not filled.
- See L<CGI::MxScreen::Form::Field> for more information.
- =item *
- There is a global hash that is made available to all screens and which is
- made persistent accross the whole session. By default, every key access
- to that hash is checked to prevent typos, and reading an unknown key is
- a fatal error (at run-time, unfortunately).
- =item *
- There are layout hooks allowing the generation of a common preamble and
- postamble section, common to a group of scripts. See L<CGI::MxScreen::Layout>
- for details.
- =item *
- The framework can be configured by loading a configuration Perl script,
- allowing easy sharing of the settings among a set of scripts, with
- possible local superseding on a script basis. See L<CGI::MxScreen::Config>
- for details.
- =item *
- All error logging is done via C<Log::Agent>, and application logging is
- done via C<Log::Agent::Logger>, which ensures the maximum flexibility.
- Logfile rotation is also supported via C<Log::Agent::Rotate>.
- Configuration of the various logging parameters is done via the
- C<CGI::MxScreen::Config> interface.
- =item *
- C<CGI::MxScreen> uses C<Carp::Datum> internally. If you have chosen to
- install a non-stripped version, you may trace parts of the module to better
- understand what is going on with the various callbacks you register.
- =back
- =head2 Flow
- Here is a high-level description of the processing flow when issuing requests
- to a C<CGI::MxScreen> script:
- =over 4
- =item *
- An initial log tracing the user (if HTTP authentication is used), the
- time since the session started, the elapsed time since the previous display,
- and the CGI query string is emitted.
- =item *
- The session context is retrieved if any, otherwise a new one is created.
- The context holds the various screen objects, the submit buttons and other
- form fields descriptions, plus all the other data stored within the
- persistent global hash.
- =item *
- Input parameters are processed, following the directives held within the
- session to validate and optionally store them in some place.
- If an error is detected, the application remains in the same state and
- the previous screen is redisplayed.
- =item *
- If no error occurred during parameter processing, the target state is computed
- based on the descriptions attached to the button that was pressed. The
- state can be given statically, or computed by a routine.
- The determined target state is composed of a screen object, plus some optional
- arguments that are to be given to its C<display> routine.
- Any processing action attached to the button is also run at that point.
- =item *
- The transition is logged, tracing the pressed button, the previous state
- and the new one.
- =item *
- If a screen change occurs (i.e. the new screen to display is not the same
- as the previously displayed one), the C<leave> routine is called on the
- old screen and C<enter> is called on the new one.
- =item *
- The enclosing form setting is emitted, and the screen's C<display> routine
- is called to actually generate the form's content. Before they output
- anything, screens are allowed to request the bouncing to some other state,
- based on some local information (but if output buffering is configured, any
- spurious output from the old screen will be cleanly discarded).
- Any other exception that can occur during C<display> is trapped and cleanly
- logged, before displaying an internal error message.
- =item *
- The application context is saved, the form is closed, and buffered output
- is emitted. A final log tracing the total time spent is emitted.
- =back
- =head2 Example
- The following example demonstrates the various common operations that need
- to be performed with C<CGI::MxScreen>.
- An important comment first: if we forget about the fact that you need
- an object per screen (which has some code overhead compared to using
- plain C<CGI>), you will need to write more I<declarative> code with
- C<CGI::MxScreen> than you would with C<CGI>, but this buys you more
- persistent state for fields, and lets you define the state transitions and
- associated processing for buttons.
- Moreover, please note that this example could be written in less code
- by using the C<CGI> module only. But C<CGI::MxScreen> is not aimed at
- simple scripts.
- Our example defines a two-state script, where one choose a color in the
- first screen, and then a week day in the second screen. The script reminds
- you about the choice made in the other screen, if any. It is possible to
- "redraw" the first screen to prove that the selection made is sticky.
- First, the whole script:
- 1 #!/usr/local/bin/perl -T
- 2
- 3 package Color; use base qw(CGI::MxScreen::Screen);
- 4
- 5 use CGI qw/:standard/;
- 6
- 7 sub init {
- 8 my $self = shift;
- 9 $self->vars->{color} = "";
- 10 }
- 11
- 12 sub display {
- 13 my $self = shift;
- 14 print h1($self->screen_title);
- 15
- 16 my $color = $self->record_field(
- 17 -name => "color",
- 18 -storage => "color",
- 19 -default => $self->vars->{color} || "Green",
- 20 -override => 1,
- 21 -values => [qw(Red Green Blue White Black Yellow Orange Cyan)],
- 22 );
- 23
- 24 print p("You told me your favorite weekday was", $self->vars->{weekday})
- 25 if exists $self->vars->{weekday};
- 26
- 27 print p("Your favorite color is", popup_menu($color->properties));
- 28
- 29 my $ok = $self->record_button(
- 30 -name => "Next",
- 31 -target => "Weekday");
- 32
- 33 my $redraw = $self->record_button(
- 34 -name => "Redraw",
- 35 -target => $self->current_screen);
- 36
- 37 print submit($ok->properties), submit($redraw->properties);
- 38 }
- 39
- 40 package Weekday; use base qw(CGI::MxScreen::Screen);
- 41
- 42 use CGI qw/:standard/;
- 43
- 44 sub init {
- 45 my $self = shift;
- 46 $self->vars->{weekday} = "";
- 47 }
- 48
- 49 sub display {
- 50 my $self = shift;
- 51 print h1($self->screen_title);
- 52
- 53 print p("You told me your favorite color was", $self->vars->{color});
- 54
- 55 my $weekday = $self->record_field(
- 56 -name => "day",
- 57 -storage => "weekday",
- 58 -default => $self->vars->{weekday} || "Mon",
- 59 -override => 1,
- 60 -values => [qw(Mon Tue Wed Thu Fri Sat Sun)],
- 61 );
- 62
- 63 print p("Your favorite weekday is", popup_menu($weekday->properties));
- 64
- 65 my $back = $self->record_button(
- 66 -name => "Back",
- 67 -target => $self->spring_screen,
- 68 );
- 69
- 70 print submit($back->properties);
- 71 }
- 72
- 73 package main;
- 74
- 75 require CGI::MxScreen;
- 76
- 77 my $manager = CGI::MxScreen->make(
- 78 -screens =>
- 79 {
- 80 'Color' => [-class => 'Color', -title => "Choose Color" ],
- 81 'Weekday' => [-class => 'Weekday', -title => "Choose Day" ],
- 82 },
- 83 -initial => ['Color'],
- 84 );
- 85
- 86 $manager->play();
- 87
- Let's study this a piece at a time:
- 1 #!/usr/local/bin/perl -T
- 2
- The classical declaration for a CGI script, in taint mode.
- 3 package Color; use base qw(CGI::MxScreen::Screen);
- 4
- This defines the first state, C<Color>. It inherits from
- C<CGI::MxScreen::Screen>, as it should.
- 5 use CGI qw/:standard/;
- 6
- We're going to use CGI routines. We could do with less than what is
- exported by the C<:standard> tag, but I did not bothered.
- 7 sub init {
- 8 my $self = shift;
- 9 $self->vars->{color} = "";
- 10 }
- 11
- The C<init()> routine is called on the screen the first time it is created.
- Upon further invocations, the same screen object will be used and re-used
- each time we need to access the C<Color> state.
- To differentiate from a plain C<CGI> script which would use hidden parameters
- to propagate the information, we store the application variable in the
- persistent hash table, which every screen can access through C<$self-E<gt>vars>.
- Here, we initialize the C<"color"> key, because any access to an unknown
- key is an error at runtime (to avoid malicious typos).
- 12 sub display {
- 13 my $self = shift;
- The C<display()> routine is invoked by the state manager on the screen
- selected for displaying.
- 14 print h1($self->screen_title);
- 15
- Prints screen title. This refers to the defined title in the manager,
- which are declared for each known screen further down on lines 78-82.
- 16 my $color = $self->record_field(
- 17 -name => "color",
- 18 -storage => "color",
- 19 -default => $self->vars->{color} || "Green",
- 20 -override => 1,
- 21 -values => [qw(Red Green Blue White Black Yellow Orange Cyan)],
- 22 );
- 23
- This declaration is very important. It tells C<CGI::MxScreen> that the
- screen makes use of a field named C<"color">, and whose value should be
- stored in the global persistent hash under the key C<"color"> (as per
- the C<-storage> indication).
- The remaining attributes are simply collected to be passed to the
- C<popup_menu()> routine via C<$color->properties> below. They could be
- omitted, and added inline when C<popup_menu()> is called, but it's best
- to regroup common things together.
- The underlying object created by C<record_field()> will be serialized
- and included in the C<CGI::MxScreen> context (only the relevant attributes
- are serialized, i.e. C<CGI> parameters such as C<-values> are not).
- This will allow the processing engine to honour some meaningful actions,
- such as validation, storage, or on-the-fly patching.
- Another important property of those objects is that C<CGI::MxScreen> will
- update the value attribute, which would be noticeable if there was no
- C<-default> line: you could query C<$color->value> to get the current
- CGI parameter value, as submitted.
- 24 print p("You told me your favorite weekday was", $self->vars->{weekday})
- 25 if exists $self->vars->{weekday};
- 26
- If we have been in the C<Weekday> screen, then the key C<"weekday"> will
- be existing in the global hash C<$self-E<gt>vars>, because it is created by
- the C<init()> routine of that object, at line 46. If we tried to access
- the key without protecting by the C<exists> test on line 25, we'd get
- a fatal error saying:
- access to unknown key 'weekday'
- This protection can be disabled if you want it so, but it is on by default.
- It will probably save you one day, but unfortunately this is a runtime check.
- 27 print p("Your favorite color is", popup_menu($color->properties));
- 28
- The above is generating the sole input of this screen, i.e. a popup
- menu so that you can select your favorite color. Note that we're passing
- C<popup_menu()>, which is a routine from the C<CGI> module, a list of
- arguments derived from the recorded field C<$color>, created at line 16.
- 29 my $ok = $self->record_button(
- 30 -name => "Next",
- 31 -target => "Weekday");
- 32
- This declaration is also very important. We're using C<record_button()>
- to declare a state transition: we wish to move to the C<Weekday> screen
- when the button I<Next> is pressed.
- 33 my $redraw = $self->record_button(
- 34 -name => "Redraw",
- 35 -target => $self->current_screen);
- 36
- The I<Redraw> button simply redisplays the current screen, i.e. there
- is no transition to another screen (state). The I<current_screen>
- routine returns the name of the current screen we're in, along with all
- the parameters we were called with, so that the transition is indeed towards
- the exact same state.
- 37 print submit($ok->properties), submit($redraw->properties);
- 38 }
- 39
- We're finishing the C<display> routine by calling the C<submit()> routine
- from the C<CGI> module to generate the submit buttons. Here again, we're
- calling C<properties()> on each button object to expand the CGI parameters,
- just like we did for the field on line 27.
- 40 package Weekday; use base qw(CGI::MxScreen::Screen);
- 41
- 42 use CGI qw/:standard/;
- 43
- This defines the second state, C<Weekday>. It inherits from
- C<CGI::MxScreen::Screen>, as it should. We also import the C<CGI>
- functions in that new package.
- Note that the name of the class need not be the name of the state.
- The association between state name and classes is done during the creation
- of the manager object (see lines 78-82).
- 44 sub init {
- 45 my $self = shift;
- 46 $self->vars->{weekday} = "";
- 47 }
- 48
- Recall that C<init()> is called when the screen is created. Since screen
- objects are made persistent for the duration of the whole session (i.e.
- while the user is interacting with the script's forms), that means the
- routine is called I<once> for every screen that gets created.
- Here, we initialize the C<"weekday"> key, which is necessary because we're
- going to use it line 58 below...
- 49 sub display {
- 50 my $self = shift;
- 51 print h1($self->screen_title);
- 52
- This is the C<display()> routine for the screen C<Weekday>. It will be
- called by the C<CGI::MxScreen> manager when the selected state is C<"Weekday">
- (name determined line 81 below).
- 53 print p("You told me your favorite color was", $self->vars->{color});
- 54
- We remind them about the color they have chosen in the previous screen.
- Note that we don't rely on a hidden parameter to propagate that value:
- because it is held in the global persistent hash, it gets part of the
- session context and is there for the duration of the session.
- 55 my $weekday = $self->record_field(
- 56 -name => "day",
- 57 -storage => "weekday",
- 58 -default => $self->vars->{weekday} || "Mon",
- 59 -override => 1,
- 60 -values => [qw(Mon Tue Wed Thu Fri Sat Sun)],
- 61 );
- 62
- The declaration of the field used to ask them about their preferred week day.
- It looks a lot like the one we did for the color, on lines 16-22, with the
- exception that the field name is C<"day"> but the storage in the context
- is C<"weekday"> (we used the same string C<"color"> previously).
- 63 print p("Your favorite weekday is", popup_menu($weekday->properties));
- 64
- The above line generates the popup. This will create a selection list
- whose CGI name is C<"day">. However, upon reception of that parameter,
- C<CGI::MxScreen> will immediately save the value to the location identified
- by the C<-storage> line, thereby making the value available to the application
- via the C<$self-E<gt>vars> hash.
- 65 my $back = $self->record_button(
- 66 -name => "Back",
- 67 -target => $self->spring_screen,
- 68 );
- 69
- We declare a button named I<Back>, which will bring us back to the screen
- we were when we sprang into the current screen. That's what C<spring_screen>
- is about: it refers to the previous stable screen. Here, since there is
- no possibility to remain in the current screen, it will be the previous screen.
- But if we had a I<redraw> button like we had in the I<Color> screen, which
- would make a transition to the same state, then C<spring_screen> will still
- correctly point to C<Color>, whereas C<previous_screen> would be C<Weekday>
- in that case.
- 70 print submit($back->properties);
- 71 }
- 72
- This closes the C<display()> routine by generating the sole submit button
- for that screen.
- 73 package main;
- 74
- We now leave the screen definition and enter the main part, where the
- C<CGI::MxScreen> manager gets created and invoked. In real life, the code
- for screens would not be inlined but stored in a dedicated file, one file
- for each class, and the CGI script would only contain the following code,
- plus some additional configuration.
- 75 require CGI::MxScreen;
- 76
- We're not "using" it, only "requiring" since we're creating an object,
- not using any exported routine.
- 77 my $manager = CGI::MxScreen->make(
- 78 -screens =>
- 79 {
- 80 'Color' => [-class => 'Color', -title => "Choose Color" ],
- 81 'Weekday' => [-class => 'Weekday', -title => "Choose Day" ],
- 82 },
- 83 -initial => ['Color'],
- 84 );
- 85
- The states of our state machine are described above. The keys of the
- C<-screens> argument are the valid state names, and each state name is
- associated with a class, and a screen title. This screen title will be
- available to each screen with C<$self-E<gt>title>, but there's no
- obligation for screens to display that information. However, the manager
- needs to know because when the C<display()> routine for the script is called,
- the HTML header has already been generated, and that includes the title.
- The act of creating the manager object raises some underlying processing:
- the session context is retrieved, incoming parameters are processed and
- silently validated.
- 86 $manager->play();
- 87
- This finally launches the state machine: the next state is computed, action
- callbacks are fired, and the target screen is displayed.
- =head2 More Readings
- To learn about the interface of the C<CGI::MxScreen> manager object,
- see L<"INTERFACE"> below.
- To learn about the screen interface, i.e. what you must implement when you
- derive your own objects, what you can redefine, what you should not override
- (the other features that you cannot redefine, so to speak), please
- read L<CGI::MxScreen::Screen>.
- To learn more about the configuration options, see L<CGI::MxScreen::Config>.
- For information on the processing done on recorded fields, read
- L<CGI::MxScreen::Form::Field> and L<CGI::MxScreen::Form::Utils>.
- For information on the state transitions that can be recorded, and the
- associated actions, see L<CGI::MxScreen::Form::Button>.
- The various session management schemes offered are described in
- L<CGI::MxScreen::Session::Medium>.
- The layering hooks allowing you to control where the generated HTML for
- the current screen goes in your grand formatting scheme are described
- in L<CGI::MxScreen::Layout>.
- Finally, the extra HTML-generating routines that are not implemented by
- the C<CGI> module are presented in L<CGI::MxScreen::HMTL>.
- =head1 SPECIFIC DATA TYPES
- This sections documents in a central place the I<state> and I<callback>
- representations that can be used throughout the C<CGI::MxScreen> framework.
- Those specifications must be serializable, therefore all callbacks
- are expressed in various symbolic forms, avoiding code references.
- Do not forget that I<all> the arguments you specify in callbacks and screens
- get serialized into the context. Therefore, you must make sure your
- objects are indeed serializable by the serializer (which is C<Storable>
- by default, well, actually C<CGI::MxScreen::Serializer::Storable>, which is
- wrapping the C<Storable> interface to something C<CGI::MxScreen> understands).
- See L<CGI::MxScreen::Config> to learn how to change the
- serializer, and L<CGI::MxScreen::Serializer> for the interface it must
- follow.
- =head2 States
- A state is a screen name plus all the arguments that are given to its
- C<display()> routine. However, the language used throughout this
- documentation is not too strict, and we tend to blurr the distinction between
- a state and a screen by forgetting about the parameters. That is because,
- in practice, the parameters are simply there to offer a slight variation
- of the overall screen dispay, but it is fundamentally the same screen.
- Anyway, a state can be either given as:
- =over 4
- =item *
- A plain scalar, in which case it must be the name of a screen, as
- configured via C<-screens> (see L<Creation Routine> below), and the
- screen's C<display()> routine will be called without any parameter.
- =item *
- An array ref, whose first item is the screen name, followed by
- arguments to be given to C<display()>. For instance:
- ["Color", "blue"]
- would represent the state obtained by calling C<display("blue")> on the
- screen object known as I<Color>.
- =back
- =head2 Callbacks
- When an argument expects a I<callback>, you may provide it under the
- foloowing forms.
- =over 4
- =item *
- As a scalar name, e.g. C<'validate'>.
- The exact interpretation of this form depends on the object where you
- specify it. Withing a C<CGI::MxScreen::Form::Button>, it specifies a
- routine to call on the screen object, without any user parameter. However,
- within a C<CGI::MxScreen::Form::Field>, it could be a routine to lookup
- within the utility namespaces. More on the latter in L<Utility Path>.
- =item *
- As a list reference, starting with a scalar name:
- ['routine', @args]
- This specifies that C<routine(@args)> should be called on the screen object.
- =item *
- As a list reference, beginning with an object reference:
- [$obj, 'routine', @args]
- which specifies that <$obj-E<gt>routine(@args)> should be called, i.e.
- the target object is no longer the screen object.
- It is available to C<CGI::MxScreen::Form::Button> objects only.
- =back
- =head1 INTERFACE
- The public interface with the manager object is quite limited.
- The main entry points are the creation routine, which configures the
- overall operating mode, and the C<play()> routine, which launches the
- state machine resolution.
- =head2 Creation Routine
- As usual, the creation routine is called C<make()>. It takes a list of
- named arguments, some of which are optional:
- =over 4
- =item C<-bgcolor> => I<color>
- Optional, sets the default background color to be used for all screens.
- If unspecified, the value is I<gray75>, aka C<"#bfbfbf">, which is the
- default background in Netscape on Unix. The value you supply will be
- used in the BGCOLOR HTML tag, so any legal value there can be used.
- For instance:
- -bgcolor => "beige"
- You may override the default background on a screen basis, as explained
- in L<CGI::MxScreen::Screen/"Creation Routine">.
- =item C<-initial> => I<scalar> | I<array_ref>
- Mandatory, defines the initial state. See L<States> above for the
- actual format details.
- The following two forms have identical effects:
- -initial => ["Color"]
- -initial => "Color"
- and both define a state I<Color> whose C<display()> routine is called
- without arguments.
- =item C<-layout> => I<layout_object>
- Optional, provides a C<CGI::MxScreen::Layout> object to be used for laying
- out the screen's HTML generated by C<display()>. See L<CGI::MxScreen::Layout>
- for details.
- =item C<-screens> => I<hash_ref>
- Mandatory, defines the list of valid states, whose class will handle it, and
- what the title of the page should be in that state. Usually, there is
- identity between a screen and a state, but via the C<display()> parameters,
- you can have the same screen object used in two different states, with a
- slightly different mode of operation.
- The hash reference given here is indexed by state names. The values must
- be array references, and their content is the list of arguments to supply
- to the screen's creation routine, plus a C<-class> argument defining the
- class to use. See L<CGI::MxScreen::Screen/"Creation Routine">.
- Example of I<hash_ref>:
- {
- 'Color' => [-class => 'Color', -title => "Choose Color" ],
- 'Weekday' => [-class => 'Weekday', -title => "Choose Day" ],
- }
- The above sequence defines two states, each implemented by its own class.
- =item C<-timeout> => I<seconds>
- Optional, defines a session timeout, which will be enforced by C<CGI::MxScreen>
- when retrieving the session context. It must be smaller than the session
- cleaning timout, if sessions are not stored within the browser.
- When the session is expired, there is an error message stating so and the
- user is invited to restart a new session.
- =item C<-version> => I<string>
- Defines the script's version. This is I<your> versioning scheme, which has
- nothing to do with the one used by C<CGI::MxScreen>.
- You should use this to track changes in the screen objects that would
- make deserialization of previous ones (from an old session) improper. For
- instance, if you add attributes to your screen objects and depend on them
- being set up, an old screen will not bear them, and your application will
- fail in mysterious ways.
- By upgrading C<-version> each time such an incompatibility is introduced,
- you let C<CGI::MxScreen> trap the error and produce an error message.
- =back
- =head2 Features
- =over 4
- =item C<internal_error> I<string>
- Immediately abort current processing and emit the error message I<string>.
- If a layout is defined, it is honoured during the generation of the
- error message.
- If you buffer STDOUT (which is the case by default), then all the output
- currently generated will be discarded cleanly. Otherwise, users might have
- to scroll down to see the error message.
- =item C<log>
- Gives you access to the C<Log::Agent::Logger> logging object. There is
- always an object, whether or not you enabled logging, if only to redirect
- all the logs to C</dev/null>. This is the same object used by
- C<CGI::MxScreen> to do its hardwired logging.
- See L<Log::Agent::Logger> to learn what can be done with such objects.
- =item C<play>
- The entry point that dispatches the state machine handling. Upon return,
- the whole HTML has been generated and sent back to the browser.
- =back
- =head2 Utility Path
- The concept of I<utility path> stems from the need to keep all callback
- specification serializable. Since C<Storable> cannot handle CODE references,
- C<CGI::MxScreen> uses function names. In some cases, we have a default
- object to call the method on (e.g. during action callbacks), or one can
- specify an object. In some other case, a plain name must be used, and you
- must tell C<CGI::MxScreen> in which packages it should look to find that name.
- This is analogous to the PATH search done by the shell. Unless you specify
- an absolute path, the shell looks throughout your defined PATH directories,
- stopping at the first match.
- Here, we're looking through package namespaces. For instance, given the
- name "is_num", we could check C<main::is_num>, then C<Your::Module::is_num>,
- etc... That's what the utility path is.
- The routine C<CGI::MxScreen::add_utils_path> must be used I<before> the
- creation of the C<CGI::MxScreen> manager, and takes a list of strings,
- which define the package namespaces to look through for field validation
- callbacks and patching routines. The reason it must be done I<before>
- is that incoming CGI parameters are currently processed during the
- manager's creation routine.
- =head1 LOGGING
- During its operation, C<CGI::MxScreen> can emit application logs. The
- amount emitted depends on the configuration, as described in
- L<CGI::MxScreen::Config>.
- Logs are emitted with the session number prefixed, for instance:
- (192.168.0.3-29592) t=0.13s usr=0.12s sys=0.01s [screen computation]
- The logged session number is the IP address of the remote machine, and the
- PID of the script when the session started. It remains constant throughout
- all the session.
- There is also some timestamping and process pre-fixing done by the
- underlying logging channel. See L<Log::Agent::Stamping> for details.
- The so-called "own" date stamping format is used by C<CGI::MxScreen>,
- and it looks like this:
- 01/04/18 12:08:22 script:
- showing the date in yy/mm/dd format, and the time in HH::MM::SS format.
- The C<script:> part is the process name, here the name of your CGI script.
- At the "debug" logging level, you'll get this whole list of logs for
- every intial script invocation:
- [main/0] t=0s u="ram" q="id=4"
- using "Mozilla/4.75 [en] (X11; U; Linux 2.4.3-ac4 i686)"
- t=0.20s usr=0.17s sys=0.01s [context restore + log init]
- t=1.15s usr=0.86s sys=0.05s [parameter init]
- t=1.71s usr=0.61s sys=0.07s [outside CGI::MxScreen]
- main()
- t=0.13s usr=0.12s sys=0.01s [screen computation]
- t=46.46s usr=43.42s sys=1.67s ["main" display]
- t=0.30s usr=0.29s sys=0.02s [context save]
- t=50.01s usr=45.53s sys=1.83s [total time] T=52.45s
- The C<t=0s> indicates the start of a new session, and C<u="ram"> signals
- that the request is made for an HTTP-authenticated user named I<ram>.
- The C<[main/0]> indicates that we're in the state called I<main>, and C<0>
- is the interaction counter (incremented at each roundtrip).
- The C<q="id=4"> traces the query string.
- The next line traces the user agent, and is only emitted at the start of
- a new session. May be useful if something goes wrong later on, so that
- you can suspect the user's browser.
- Then follows a bunch of timing lines, each indicating what was timed
- in trailing square brackets. The final total summs up all the other lines,
- and also provides a precious C<T=52.45s> priece of statistics, measuring the
- total wallclock time since the script startup. This helps you evaluate
- the overhead of loading the various modules.
- The single C<main()> line traces the state information. Here, since this
- is the start of a new session, we enter the initial state and there's no
- state transition.
- Note the very large time spent by the C<display()> routine for that
- screen. This is because C<Carp::Datum> was on, and there was a lot of
- activity to trace.
- Compare this to the following log, where the user pressed a button
- called I<refresh>, which simply re-displays the same screen, and where
- C<Carp::Datum> was turned off:
- [main/1] t=1m11s d=19s u="ram"
- t=0.90s usr=0.83s sys=0.08s [context restore + log init]
- t=0.01s usr=0.00s sys=0.00s [parameter init]
- t=0.02s usr=0.02s sys=0.00s [outside CGI::MxScreen]
- main() -> main() on "refresh" pressed
- t=0.02s usr=0.01s sys=0.00s [screen computation]
- t=0.56s usr=0.58s sys=0.00s ["main" display]
- t=0.05s usr=0.05s sys=0.00s [context save]
- t=1.56s usr=1.50s sys=0.08s [total time] T=3.24s
- The new C<d=19s> item on the first line indicates the elapsed time since
- the end of the first invocation of the script, and this new one. It is
- the time the user contemplated the screen before pressing a button.
- Note that there is no C<q="id=4"> shown: C<CGI::MxScreen> uses POST requests
- between its invocations, and does not propagate the initial query string.
- It is up to you to save any relevant information into the context.
- The following table indicates the logging level used to emit each of the
- logging lines outlined above:
- Level Logging Line Exerpt
- ------- --------------------------------
- warning [main/1] ...
- info using "Mozilla/4.75...
- debug ... [context restore + log init]
- debug ... [parameter init]
- debug ... [outside CGI::MxScreen]
- notice main() -> main() on "refresh"...
- debug ... [screen computation]
- debug ... ["main" display]
- debug ... [context save]
- info ... [total time] T=3.24s
- All timing logs but the last one summarizing the total time are made at
- the I<debug> level. All state transitions (button press, or even bounce
- exceptions) are logged at the I<notice> level. Invocations are logged
- at the I<warning> level, in order to trace them more systematically.
- =head1 BUGS
- There are still some rough edges. Time will certainly help polishing them.
- If you find any bug, please contact both authors with the same message.
- =head1 HISTORY AND CREDITS
- C<CGI::MxScreen> began when Raphael Manfredi, who knew next to nothing about
- CGI programming, stumbled on the wonderful C<MxScreen> program, by
- Tom Christiansen, circa 1998. It was a graphical query compiler for his
- I<Magic: The Gathering> database. I confess I learned eveything there was to
- learn about by studying this program. I owed so much to that C<MxScreen>
- script that I decided to keep the name in the module.
- However, C<MxScreen> was a single application, very well written, but not
- reusable without doing massive cut-and-paste, and rather monolithic.
- The first C<CGI::MxScreen> version was written by Raphael Manfredi to
- modularize the various concepts in late 1998 and early 1999. It was
- never published, and was too procedural.
- In late 1999, I introduced my C<CGI::MxScreen> to Christophe Dehaudt.
- After studying it for a while, he bought the overall concept, but
- proposed to drop the procedural approach and switch to a pure object-oriented
- design, to make the framework easier to work with. I agreed.
- The current version of C<CGI::MxScreen> is the result of a joint work
- between us. Christophe did the initial experimenting with the new ideas,
- and Raphael consolidated the work, then wrote the whole documentation
- and regression test suite. We discussed the various implementation
- decisions together, and although the result is necessarily a compromise,
- I (Raphael) believe it is a good compromise.
- We managed to use C<CGI::MxScreen> in the industrial development of a
- web-based project time tracking system. The source was well over 20000
- lines of pure Perl code (comments and blank lines stripped), and we reused
- more than 50000 lines of CPAN code. I don't think we would have succeeded
- without C<CGI::MxScreen>, and without CPAN.
- The public release of C<CGI::MxScreen> was delayed more than a year because
- the dependencies of the module needed to be released first, and also
- we were lacking C<CGI::Test> which was developped only recently. Without
- it, writing the regression test suite of C<CGI::MxScreen> would have been
- a real pain, due to its context-sensitive nature. See L<CGI::Test> if
- you're curious.
- =head1 AUTHORS
- The original authors are
- Raphael Manfredi F<E<lt>Raphael_Manfredi@pobox.comE<gt>>
- and
- Christophe Dehaudt F<E<lt>Christophe.Dehaudt@teamlog.frE<gt>>.
- Send bug reports, suggestions, problems or questions to
- Jason Purdy F<E<lt>Jason@Purdy.INFOE<gt>>
- =head1 SEE ALSO
- CGI::MxScreen::Config(3), CGI::MxScreen::Screen(3), CGI::MxScreen::Layout(3).
- =cut
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement