./TravEpi/TRhIP/App.pm
package MGH_Biostat::TravEpi::TRhIP::App;
use Modern::Perl '2012';
use experimental 'switch';
use utf8;
use Plack::Request;
use Template::Simple;
use File::Slurp qw(slurp read_dir);
use Encode;
use URI;
use HTML::FillInForm;
use HTTP::AcceptLanguage;
use Digest::SHA qw(sha256_hex);
use Set::Tiny qw(set);
use Package::Alias 'M_B::Countries' => 'MGH_Biostat::TravEpi::SimpleRulesBase::Countries';
use MGH_Biostat::TravEpi::SimpleRulesBase::Rules;
use MGH_Biostat::TravEpi::SimpleRulesBase::Translator;
use MGH_Biostat::TravEpi::SimpleRulesBase::DynamicText;
my $INCLUDE_DEBUG_INFORMATION = 0;
# #####################################################################################
# Object creation and initialization
# #####################################################################################
sub new {
my $class = shift;
my $self = bless {}, $class;
# template engine
$self->{'tmpl'} = Template::Simple->new(
token_re => qr/[.\w-]+?/,
search_dirs => [ qw/templates/ ],
disallow_template_strings => 1,
template_binmode => ':encoding(UTF-8)',
pre_delim => qr/\<\?ts/,
post_delim => qr/\?\>/,
);
# create the lookout engines
$self->{'translator'} = MGH_Biostat::TravEpi::SimpleRulesBase::Translator->new();
# for each valid language, create a template of the questions and recommendations in that language
# enclosed in the website shell
my $shell_template = ${ $self->{'tmpl'}->_get_template('shell') };
foreach my $class (qw(questions recommendations)) {
my $class_template = ${ $self->{'tmpl'}->_get_template($class) };
my $composed_template = ( $shell_template =~ s/!!\$content/$class_template/r );
foreach my $l ( $self->{'translator'}->valid_languages()->elements() ) {
$self->{'tmpl'}->add_templates( { $class . '.' . $l => $self->{'translator'}->translate_template( $l, $composed_template ) } );
}
}
# load the rules
$self->{'rules'} = MGH_Biostat::TravEpi::SimpleRulesBase::Rules->new();
return $self;
}
# #####################################################################################
# Page handlers
# #####################################################################################
sub dispatch {
my $self = shift;
my $env = shift;
my $req = Plack::Request->new($env);
my $params = $req->parameters;
my $_known_parameters = [ qw(hash age destination1 destination2 destination3 destination4 submit) ];
# get the preferred language
my $lang = $params->{'lang'}
|| HTTP::AcceptLanguage->new( $req->header('Accept-Language') )->match( @{ $self->{'translator'}->ordered_languages() } )
|| $self->{'translator'}->default_language()
;
if ( !$self->{'translator'}->is_valid_language($lang) ) {
die("Invalid language specified.\n");
}
my $data = {
'_lang' => $lang,
'_othlang_include' => [],
'_errors' => '',
'_template' => 'questions',
};
DISPATCH_LOGIC: {
# if there is no hash provided, this is the start of a new request
$data->{'_hash'} = $params->{'hash'};
if ( !defined( $data->{'_hash'} ) or ( $data->{'_hash'} eq '' ) ) {
$data->{'_hash'} = sha256_hex( 'trhip' . localtime() . $$ . rand(10000) );
last DISPATCH_LOGIC;
}
# if the "submitted" value doesn't exist, we're just changing languages
# this should never be reached (right now), because we don't include the hash
# in the keys to encode in the url
# (ie, this may be a useless check and should possibly be removed. I'm leaving it
# in case we decide we do want to track language changes for the questions page.)
my $submitted = $params->{'submit'};
if ( !$submitted ) {
last DISPATCH_LOGIC;
}
# copy the original parameters into the data structure
$data->{'_parameters'} = { map { $_ => ( $params->{$_} // '' ) } @$_known_parameters };
# check for parameter errors
$data->{'_errors'} = _check_parameter_errors( $data->{'_parameters'} );
if ( $data->{'_errors'} ) {
last DISPATCH_LOGIC;
}
# at this point, everything in good, so generate some recommendations
$data->{'_template'} = 'recommendations';
$data->{'_othlang_include'} = $_known_parameters;
$data->{'_variables'} = _synthesize_variables( $data->{'_parameters'} );
$data->{'_generated'} = $self->recommendations( $data->{'_variables'}, $data );
}
foreach my $l ( @{ $self->{'translator'}->ordered_languages() } ) {
next if ( $l eq $lang );
my $u = URI->new('trhip');
$u->query_form( 'lang' => $l, map { $_ => $data->{'_parameters'}{$_} } grep { $data->{'_parameters'}{$_} } @{ $data->{'_othlang_include'} } );
push @{ $data->{'_other_languages_list'} }, { 'uri' => $u->as_string(), 'text' => $self->{'translator'}->get_inlanguage($l) };
}
my $out = {
'_lang' => $data->{'_lang'},
'_hash' => $data->{'_hash'},
'_other_languages_list' => $data->{'_other_languages_list'},
'_errors' => $data->{'_errors'},
'_generated' => $data->{'_generated'} // '',
'_debug' => '',
};
if ($INCLUDE_DEBUG_INFORMATION) {
use Data::Dump 'pp';
$out->{'_debug'}{'_debug_text'} =
"ENV:\n" . pp($env) . "\n\n" .
"PARAMS:\n" . pp($params) . "\n\n" .
"DATA\n" . pp($data) . "\n\n" .
"OUT:\n" . pp($out)
;
}
# log the action
$env->{'gten.logger'}->log( $env, $data->{'_hash'}, $lang, $data->{'_template'} );
my $template = $data->{'_template'};
my $res = $req->new_response(
200,
[ Content_Type => 'text/html; charset=utf-8' ],
encode( 'utf-8', HTML::FillInForm->fill( $self->{'tmpl'}->render( "${template}.${lang}", $out ), $data->{'_parameters'} ) )
);
return $res->finalize();
}
sub recommendations {
my $self = shift;
my ( $vars, $info ) = @_;
my $data = {};
# put the summary variables in at the top
$data->{'summary'} = _add_summary_variables($vars);
# run the rules
$self->{'rules'}->process_rule( 'master', $vars, $data );
# add the destination links
$data->{'cdc_links'} = newdt( 'destlinks', $vars->{'dests'}, { 'po-context' => 'Sort' } );
# format any imported destinations
$self->_format_dynamic_text( $data, $info->{'_lang'} );
# it's a bit of a hack, but we want to add the hash value to the redirects, so we can use it at a higher level
# @WARNING: this is a breach of data encapsulation...
foreach ( @{ $data->{'cdc_links'} } ) {
$_->{'dest_link'} .= '?' . $info->{'_hash'};
}
return $data;
}
sub showrules {
my $self = shift;
my $env = shift;
my $req = Plack::Request->new($env);
# get the template file
my $tmpl = ${ $self->{'tmpl'}->_get_template('recommendations.en-US') };
# although it would be great to use DOM::Tiny here to go through the instructions,
# it doesn't work because it insists on making everything correct when it is inserted,
# thus adding a '<div>' tag after the START instruction introduces a '</div>' where
# we don't want it.
# Thus, we are stuck falling back on regex.
$tmpl =~ s/(<\?ts START .+?\?>)/$1<div class="rule-enclosure"><?ts _rule ?>/g;
$tmpl =~ s/(<\?ts END .+?\?>)/<\/div>$1/g;
# get the rule descriptions
my $data = {
'_generated' => {
'master' => $self->{'rules'}->describe_rule('master'),
'summary' => { 'age' => '[age]', 'dests' => '[list of destinations]' },
'cdc_links' => { 'dest_link' => '#', 'dest_link_name' => '[list of destinations]' },
},
};
my $res = $req->new_response( 200, [ Content_Type => 'text/html; charset=utf-8' ] );
$res->body( encode( 'utf-8', ${ $self->{'tmpl'}->render( \$tmpl, $data ) } ) );
return $res->finalize();
}
sub showlogs {
my $self = shift;
my $env = shift;
my $req = Plack::Request->new($env);
my $mindate = $req->param('min') || '0000-00-00';
my $maxdate = $req->param('max') || '9999-99-99';
use List::Util qw(minstr);
use Socket;
# read in all of the log files
my @logs = grep { m/applog/; } read_dir( './logs', prefix => 1 );
my $data = {};
foreach my $f (@logs) {
foreach my $line ( slurp( $f, 'chomp' => 1 ) ) {
next unless ( $line =~ m/^keep:/ );
$line =~ s/^keep: //;
my ( $datetime, $hash, $ip, $lang, $action, $useragent, $extra ) = split( / ; /, $line );
$useragent //= '';
# process the date/time string
my ( $date, $time ) = split( / /, $datetime );
next if ( $date lt $mindate );
next if ( $date gt $maxdate );
push @{ $data->{'raw'}{$date}{$ip}{$hash} }, [ $time, $lang, $action ];
push @{ $data->{'ips'}{$ip} }, $useragent;
}
}
# clean up the ip to user agent mapping
foreach my $fip ( keys %{ $data->{'ips'} } ) {
$data->{'ips'}{$fip} = set( grep { $_; } @{ $data->{'ips'}{$fip} } );
}
foreach my $date ( sort keys %{ $data->{'raw'} } ) {
my $processed = {
'date' => $date,
'look' => { count => 0 },
'request' => { count => 0, numredirects => 0 },
'items' => [],
};
foreach my $ip ( sort keys %{ $data->{'raw'}{$date} } ) {
foreach my $session ( keys %{ $data->{'raw'}{$date}{$ip} } ) {
my $temp;
$temp->{'time'} = '99:99:99';
foreach my $ref ( @{ $data->{'raw'}{$date}{$ip}{$session} } ) {
my ( $time, $lang, $action ) = @$ref;
$temp->{'langs'}{$lang}++ unless ( $lang eq '!na' );
$temp->{'time'} = minstr( $temp->{'time'}, $time );
$temp->{'typecount'}{$action}++;
if ( ( $action eq 'questions' ) and !exists( $temp->{'type'} ) ) {
$temp->{'type'} = 'look';
}
if ( $action eq 'recommendations' ) {
$temp->{'type'} = 'request';
}
if ( $action =~ m/^redirect: (.+)/ ) {
$temp->{'redirects'}{$1}++;
}
}
push @{ $processed->{'items'} }, $temp;
$processed->{ $temp->{'type'} }{'count'}++;
foreach my $country ( keys %{ $temp->{'redirects'} } ) {
$processed->{'request'}{'redirects'}{'hash'}{$country} += $temp->{'redirects'}{$country};
}
if ( scalar( keys %{ $temp->{'redirects'} } ) ) {
$processed->{'request'}{'numredirects'}++;
}
my $langmax = 0;
my $langname = '';
foreach my $lang ( keys %{ $temp->{'langs'} } ) {
if ( $temp->{'langs'}{$lang} > $langmax ) {
$langmax = $temp->{'langs'}{$lang};
$langname = $lang;
}
}
$processed->{ $temp->{'type'} }{'langs'}{'hash'}{$langname}++;
$processed->{ $temp->{'type'} }{'ips'}{'hash'}{$ip}++;
}
}
foreach my $type (qw/look request/) {
foreach my $subtype (qw/langs ips redirects/) {
if ( scalar( keys %{ $processed->{$type}{$subtype}{'hash'} } ) ) {
$processed->{$type}{$subtype}{'array'} = _hash_to_hasharray( $processed->{$type}{$subtype}{'hash'}, 'key', 'count' );
}
else {
$processed->{$type}{$subtype} = '';
}
}
if ( ref( $processed->{$type}{'ips'} ) ) {
foreach ( @{ $processed->{$type}{'ips'}{'array'} } ) {
$_->{'useragents'} = join( '

' => $data->{'ips'}{ $_->{'key'} }->members() );
if ( !defined( $_->{'useragents'} ) or ( $_->{'useragents'} eq '' ) ) {
$_->{'useragents'} = '(none)';
}
}
}
}
$data->{'processed'}{'total'}{'look'} += $processed->{'look'}{'count'};
$data->{'processed'}{'total'}{'request'} += $processed->{'request'}{'count'};
$data->{'processed'}{'total'}{'redirects'} += $processed->{'request'}{'numredirects'};
push @{ $data->{'processed'}{'date'} }, $processed;
}
$data->{'processed'}{'mindate'} = $mindate;
$data->{'processed'}{'maxdate'} = $maxdate;
# get the template file
my $tmpl = slurp( 'templates/log.tmpl', binmode => ':encoding(UTF-8)' );
my $res = $req->new_response( 200, [ Content_Type => 'text/html; charset=utf-8' ] );
$res->body( encode( 'utf-8', ${ $self->{'tmpl'}->render( \$tmpl, $data->{'processed'} ) } ) );
return $res->finalize();
}
sub _hash_to_hasharray {
my ( $hashref, $keyname, $valname ) = @_;
my $ret = [];
foreach my $k ( sort keys %$hashref ) {
push @{$ret}, { $keyname => $k, $valname => $hashref->{$k} };
}
return $ret;
}
# #####################################################################################
# Parameter helper functions
# #####################################################################################
# verify that there are no missing values
sub _check_parameter_errors {
my ($params) = @_;
my $errors = {};
if ( ( !$params->{'age'} ) or ( $params->{'age'} !~ m/^\s*\d+\s*$/ ) ) {
$errors->{'age'} = {};
}
if ( !scalar( grep { $_ =~ m/\S/ } @{$params}{qw(destination1 destination2 destination3 destination4)} ) ) { # ! note hash slice
$errors->{'dests'} = {};
}
return ( %$errors ? $errors : '' );
}
# turn the parameters into the variables (ie, the sets, and undef if missing)
sub _synthesize_variables {
my ($params) = @_;
my $dests = set( [ grep { $_ =~ m/\S/; } @{$params}{qw(destination1 destination2 destination3 destination4)} ] ); # ! note hash slice
my $vars = {};
$vars->{'age'} = ( 0 + $params->{'age'} ) // undef;
$vars->{'dests'} = M_B::Countries::mapped_countries($dests);
if ( $vars->{'dests'}->is_empty() ) { $vars->{'dests'} = undef; }
return $vars;
}
sub _add_summary_variables {
my ($vars) = @_;
# we know that these variables exist, because otherwise there would have been an error!
return {
'age' => newdt( 'age', $vars->{'age'} ),
'dests' => newdt( 'countries', $vars->{'dests'} ),
};
}
# go through the generated rule tree and realize any dynamic replacements
sub _format_dynamic_text {
my $self = shift;
my ( $tree, $lang ) = @_;
foreach my $k ( keys %$tree ) {
if ( ref( $tree->{$k} ) eq 'MGH_Biostat::TravEpi::SimpleRulesBase::DynamicText' ) {
$tree->{$k} = $self->{'translator'}->translate_dynamic_text( $tree->{$k}, $lang );
}
elsif ( ref( $tree->{$k} ) eq 'HASH' ) {
$self->_format_dynamic_text( $tree->{$k}, $lang )
}
}
}
1;
__END__