package LIMS::Controller; use 5.006; our $VERSION = '1.6'; { package lims_controller; use LIMS::Database::Util; use LIMS::Web::Interface; # web methods come first in the inheritance tree our @ISA = qw( lims_interface lims_database ); sub DESTROY { my $self = shift; $self->close_log; $self->disconnect_dbh; $self->SUPER::DESTROY; } sub finish { my $self = shift; $self->param_forward; $self->print_footer; $self->disconnect_dbh; $self->close_log; } ### login/session methods ### sub check_login { my $self = shift; my $q = $self->get_cgi; if (my $db_user_name = $q->param('user_name')){ if (my $db_user_pass = $q->param('password')){ $self->check_user_pass; } elsif (my $personnel_id = $q->param('personnel_id')){ $self->check_session; } else { $self->db_error('No password was entered'); } } else { $self->db_error('No user name was entered'); } if ($self->db_error){ $self->print_errors; $self->print_footer; return undef; # bad login } else { return 1; # login OK } } sub check_session { my $self = shift; my $epoch_time = Date::EzDate->new()->{'epoch second'}; my $sess_start_secs; my $session_length = $self->session_length; # get session information from db if (my $user_session = $self->get_user_session) { # check time since last session activity $sess_start_secs = Date::EzDate->new( $self->session_time )->{'epoch second'}; my $session_duration = $epoch_time - $sess_start_secs; if ( $session_duration > $session_length ) { $self->db_error('session timed out'); } #Ęcheck the user's ip address matches that in the db if ( $self->session_ip ne $self->current_ip ) { $self->db_error('ip error'); } } else { $self->db_error('session closed'); } # so long as there aren't any errors, update the session to NOW() unless( $self->db_error ){ # compare session time in db with that from cgi if (( $sess_start_secs > Date::EzDate->new( $self->current_sess_time )->{'epoch second'}) && ( $self->back_sensitive)) { # is an 'old' session $self->standard_error("Data from this page has already been entered into the database.","Please don't use the browser's 'back' button after submitting a form"); $self->kill_pipeline; } else { $self->update_session; } } } sub update_session { my $self = shift; $self->alter_session_id(1); } sub close_session { my $self = shift; $self->alter_session_id(0); } sub log_out { my $self = shift; $self->close_session; my $q = $self->get_cgi; $q->delete_all(); $q->param(-name=>'logout',-value=>1); } sub alter_session_id { my $self = shift; if (@_) { my $state = shift; my $date = Date::EzDate->new(); my $mysql_time = $date->{'{year}/{%m}/{%d} %T'}; # unix style %Y actually returns 2-digit year my $ip_address = $self->current_ip; my $usr_info_obj = $self->get_user_info; my $session_id = ($state) ? $ip_address.",".$mysql_time : ''; $usr_info_obj->session_id($session_id); $usr_info_obj->update(); $usr_info_obj->dbi_commit; $self->session_id($usr_info_obj->session_id); } } sub current_ip { my $self = shift; if(defined $ENV{'HTTP_PC_REMOTE_ADDR'}){ # is mac os x server return $ENV{'HTTP_PC_REMOTE_ADDR'}; } else { # use standard cgi remote host call my $q = $self->get_cgi; return $q->remote_host(); } } sub system_ip { use Net::Address::IPv4::Local; my $ip = Net::Address::IPv4::Local->public; return $ip; } sub current_sess_time { # from cgi my $self = shift; $self->session_arry($self->session_id,1); } sub session_ip { # from db my $self = shift; $self->session_arry($self->get_user_session,0); } sub session_id { my $self = shift; my $q = $self->get_cgi; if (@_) { $q->param('session_id',shift); } else { $q->param('session_id'); } } sub session_time { # from db my $self = shift; $self->session_arry($self->get_user_session,1); } sub session_arry { my $self = shift; my $session = shift; # e.g. 127.0.0.1,2007/10/22 12:2:14 my @session = split (/,/, $session); if (@_) { my $element = shift; return($session[$element]); } else { return(@session); } } sub get_user_session { my $self = shift; my $user_obj = $self->get_user_info; return $user_obj->session_id; # e.g. 127.0.0.1,2007/10/22 12:2:14 } sub get_user_info { my $self = shift; unless ($self->{ _user_info }){ $self->{ _user_info } = DBLoader::UserInformation->retrieve($self->personnel_id); } return $self->{ _user_info }; } sub check_user_pass { my $self = shift; my $user_name = $self->db_user_name; my $user_pass = $self->db_user_pass; my $statement = " SELECT personnel_id FROM user_information WHERE full_name = '$user_name' AND password = OLD_PASSWORD(?) "; if (my $personnel_id = $self->sql_fetch_bindparam($statement,$user_pass)){ my $q = $self->get_cgi; $q->delete('password','Login'); $self->personnel_id($personnel_id); $self->update_session; } else { $self->db_error('login failed'); } } # user_name and user_pass are only set at login sub db_user_name { my $self = shift; my $q = $self->get_cgi; $q->param('user_name'); } sub db_user_pass { my $self = shift; my $q = $self->get_cgi; $q->param('password'); } # personnel_id set by check_login() sub personnel_id { my $self = shift; my $q = $self->get_cgi; if (@_) { $q->param('personnel_id',shift); } else { $q->param('personnel_id'); } } sub text_errors { my $self = shift; return $self->get_error_string($self->db_error,$self->standard_error); } sub print_db_errors { my $self = shift; return unless (my $aErrors = $self->db_error); if ($self->has_cgi){ my $q = $self->get_cgi; $self->print_header unless ($self->title_printed); print $q->h2("The following errors were reported:"), $q->start_p({-class=>'lims_error'}); for my $error (@$aErrors){ print $q->em($error), $q->br; } print $q->end_p; } else { print $self->get_error_string($aErrors); } } sub any_error { my $self = shift; if (($self->standard_error) || ($self->db_error)){ return 1; } else { return; } } sub print_errors { my $self = shift; $self->print_db_errors; $self->print_standard_errors; } sub clear_all_errors { my $self = shift; $self->clear_db_errors; $self->clear_standard_errors; } sub write_log { my $self = shift; my $oLog_File = $self->get_log_file; $oLog_File->add_text(@_); } sub close_log { my $self = shift; if ($self->is_log_open){ my $oLog_File = $self->get_log_file; $oLog_File->close_filehandle; } } sub log_open { my $self = shift; $self->{ _log_open }++; } sub is_log_open { my $self = shift; $self->{ _log_open }; } sub get_log_file { require Microarray::File; my $self = shift; unless (defined $self->{ _log_file }) { $self->{ _log_file } = log_file->new($self->create_storage_path('log_file')); $self->log_open; } $self->{ _log_file }; } sub save_file { my $self = shift; my $var = shift; my ($filehandle,$file_name); if (ref $var){ # isa filehandle $file_name = shift; $filehandle = $var; } else { # var isa file param name if (@_){ ($filehandle,$file_name) = $self->upload_file($var,shift); } else { ($filehandle,$file_name) = $self->upload_file($var); } } my $file_id = $self->filehandle_to_blob($filehandle,$file_name); return ($file_id, $file_name); } } 1; __END__ =head1 NAME LIMS::Controller - Perl object layer controlling the LIMS database and its web interface =head1 SYNOPSIS use LIMS::Controller; # login and session control my $database = database->new('My CGI Page'); my $database = database->new_guest('My CGI Page'); # for pages where no user/pass required # embedded DBI and CGI objects my $dbh = database->get_dbh; my $q = database->get_cgi; # simplified database queries/inserts $database->sql_fetch_singlefield($statement); my $insert_id = $database->insert_into_table($table,$values); # error handling for DBI functions $database->kill_pipeline if ($database->any_error); # ....and it even tidies up after itself $database->finish; =head1 DESCRIPTION LIMS::Controller is a versatile object-oriented Perl module designed to control a LIMS database and its web interface. Inheriting from the L and L classes, the module provides automation for many core and advanced functions required of a web/database object layer, enabling rapid development of Perl CGI scripts. =head1 WRITING A LIMS::Controller PLUG-IN First, look at the L module. This is the plug-in written along-side LIMS::Controller, to control our laboratory's CGH-microarray LIMS database. There are many standard methods in there that you will probably want/need in your own module. For most situations, simply editing the config file (see below) to set defaults for your own system will suffice to provide you with a working LIMS. =head2 SETTING UP YOUR DATABASE There are several parameters that must be set in a config file. For our L plug-in, running on a UNIX/LINUX type system, the path to this file is set as '/etc/pipeline.conf' and defined in L. Most of the parameters are self-explanatory, including database hostname, database login, the base URL for the web server, etc. The database being controlled by the module must have a table called C<'USER_INFORMATION'> as described in the accompanying documentation. This table handles user login at the WEB/CGI level. At the database level, a user should be defined with relevant privileges for all required WEB/CGI operations, and the user name and password for this account must be set in the config file. If, for some reason, you need to set other privilege levels beyond these, we suggest you do this at the WEB server level on a script-by-script basis. For instance, you might want to provide browse-only access to some members of staff, or reserve some admin functions for other members of staff. =head1 METHODS =head2 Basic functions There are actually only a few methods that are used on a regular basis in a CGI script. =over 4 =item B All create a new LIMS::Controller object, but of subtly different flavours. C creates the embedded DBI and CGI objects, and requires two form parameters; C<'user_name'>, and either C<'password'> or C<'session_id'> which it verifies I the C<'USER_INFORMATION'> table in your database. The method C is similar but does not require the user parameters and does not verify login. The method C returns a new object without CGI/DBI or user login. So the initial login page to the system would use the C method, and provide a form to enter a C and C. A script receiving a valid C combination will then return an object from the C method, and create a valid session_id. Subsequently, the C method will return an object from a valid C combination. =item B These methods return the embedded DBI database handle and CGI object respectively. The database handle should not be required, since most DBI functions are handled within LIMS::Controller. It is recommended that you use the object-oriented style of calling CGI methods, although you I don't HAVE to. =item B Prevents the user from using the back button on their browser by rejecting an old C. =item B Causes the script to die if any errors are thrown, printing out all errors and issuing a C call to the database. =item B Tidies up at the end of a script; prints a page footer (if there is one), forwards parameters if not already performed, disconnects from the database and closes a log file (if there is one open). =item B Returns the page title, set in the C and C methods. =back =head2 DBI Functions Most of these methods are simply wrappers for DBI calls, catching possible errors so that the way they are reported can be controlled in the CGI script. Why not use L? Well you can if you prefer - table classes are already loaded I L. =head3 Simple SQL 'fetch' methods Methods fetching the results of C