ONO::ToolBox::LogfileReader

package ONO::ToolBox::LogfileReader;
################################################################################
# COPYRIGHT / LICENSE #
################################################################################
#
# This file is part of the ONO Software Project.
#
# Copyright (C) 2000-2025 Jos KIRPS [ www.kirps.com | jos_AT_kirps_DOT_com ]
# and The Joopita Project [ www.joopita.org | contact_AT_joopita_DOT_com ]
#
# This file, as well as other parts of the ONO Software Project or related
# elements, are FREE SOFTWARE available under the ARTISTIC LICENSE 2.0.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# For the full license, see /ono/osr/license/LICENSE.txt, or write to
# jos_AT_kirps_DOT_com or contact_AT_joopita_DOT_com.
#
################################################################################
# END OF COPYRIGHT / LICENSE, HERE COMES THE CODE ... #
################################################################################


use strict;

use ONO::IO;

use ONO::Lib::DateTime::ToolBox;
use ONO::Lib::Data::Monitor;

use ONO::Lib::UI::MSG;
use ONO::Lib::UI::Page;

use ONO::FW::User::Init;
use ONO::FW::Apps::ToolBox;

###############################################################################
# ONO
###############################################################################

#: This module offers tools to read standard ONO logfiles, as will as several
#: host OS logfile formats.

sub reader {

my (
$self,
$file,
$link,
$form,
$vars_ref,
$BLK_ref,
$switches,
$setup_ref,
) = @_;

#: The standard ONO logfile reader, which will read logfiles written by the
#: ONO_ToolBox_Logfile module.
#:
#:
#: Available switches:
#:
#: -A app mode (for e-learning apps)
#: -B buttons (requires -S and $setup{'logline_buttons'})
#: -c colorize
#: -f flat_top (-h mode only)
#: -F form/filter off (required when running within some evil forms...)
#: -h human mode (with user icons etc)
#: -H 100 results per page
#: -I don't show the IP
#: -M monitor bad words / bad content
#: -R round box style
#: -S use %setup
#: -t ten results per page
#: -X unix log mode (auto-enabled when running access_log or error_log)
#:
#: ono_toolbox_logfilereader_topbar_id

my %vars = %$vars_ref;
my %BLK = %$BLK_ref;
my %setup;
if ($switches =~ /S/ && $setup_ref) {
%setup = %$setup_ref;
}

my $num = 20;
if ($switches =~ /H/) {
$num = 100;
}
if ($switches =~ /t/) {
$num = 10;
}

my $APPS_BASE = ONO::FW::Apps::ToolBox->apps_base();

my (
$sec,$min,$hour,
$mday,$mon,$year,
$wday,$yday,
$timestamp,$isdst,
) = ONO::Lib::DateTime::ToolBox->get;

my $link_nav = $link;
my @filters = ('date','time','user','text','id','ip');
foreach my $filter (@filters) {
if ($vars{"logfile_filter_$filter"}) {
$link_nav .= qq~&logfile_filter_$filter=$vars{"logfile_filter_$filter"}~;
}
}

$link_nav .= "&page";

my ($LOG,$LINES,$BUTTONS,$total,$counter,$NAV_STYLE,$last_used,$hide_x,$BOPT);

if ($switches =~ /R/) {

$BOPT = "_black";
$NAV_STYLE = "bg_paper p10";

}

my @lines;

if ($file =~ /^(access_log|error_log)$/) {

@lines = reverse ONO::IO->sys($file);
$switches =~ s~h~~g;
$switches .= "X";
$hide_x = " hide";

} else {

@lines = reverse ONO::IO->list($file);

}

foreach my $line (@lines) {

my @lp = split(/\;/,$line);

if (
($switches =~ /X/) ||
(
$lp[0] &&
(!$vars{'logfile_filter_date'} || $lp[1] =~ /$vars{'logfile_filter_date'}/) &&
(!$vars{'logfile_filter_time'} || $lp[2] =~ /$vars{'logfile_filter_time'}/) &&
(!$vars{'logfile_filter_user'} || $lp[4] =~ /$vars{'logfile_filter_user'}/i) &&
(!$vars{'logfile_filter_text'} || $lp[5] =~ /$vars{'logfile_filter_text'}/i) &&
(!$vars{'logfile_filter_id'} || $lp[6] =~ /$vars{'logfile_filter_id'}/i) &&
(!$vars{'logfile_filter_ip'} || $lp[3] =~ /$vars{'logfile_filter_ip'}/i)
)

) {

$total++;

if ($counter < $num && $total > (($vars{'page'}-1)*$num)) {

if ($switches =~ /I/) {
$lp[3] = "n/a";
}

if ($switches =~ /X/) {

if ($file eq "access_log") {

$line =~ s~^(.*?) - ~~;
$lp[3] = $1;

$line =~ s~\[(.*?)\]~~;
$lp[1] = $1;

$lp[1] =~ s~:(.*)~~;
$lp[2] = $1;
$lp[2] =~ s~\+(.*)~~;

$line =~ s~\"(.*?)\"~~;
$lp[5] = $1;

$lp[0] = "";

}

if ($file eq "error_log") {

$line =~ s~\[(.*?)\]~~;
$lp[1] = $1;

if ($lp[1] =~ /\./) {
$lp[1] =~ s~(.*?) (.*?) (.*?) (.*?)\:(.*?)\:(.*?)\.(.*?) (.*)~~;
$lp[1] = "$3/$2/$8";
$lp[2] = "$4:$5:$6";
} else {
$lp[1] =~ s~(.*?) (.*?) (.*?) (.*?)\:(.*?)\:(.*?) (.*)~~;
$lp[1] = "$3/$2/$7";
$lp[2] = "$4:$5:$6";
}

if ($line =~ s~\[client (.*?):(.*?)\]~~) {
$lp[3] = $1;
}

$line =~ s~\[cgi:(.*?)\]~~;
$line =~ s~\[pid (.*?)\]~~;
$line =~ s~AH01215\: ~~;
$line =~ s~\[(.*?) (.*?) (.*?) (.*?):(.*?):(.*?) (.*?)\] ~~;

$lp[5] = $line;
$lp[5] =~ s~\,(.*)~<div class="small col6">$1</div>~;

}

} else {

$lp[0] = ONO::Lib::DateTime::ToolBox->get($lp[0],0,"iN",$BLK{'lang'});

$lp[1] = substr($lp[1],6,2).".".substr($lp[1],4,2).".".substr($lp[1],0,4);
$lp[2] = substr($lp[2],0,2).":".substr($lp[2],2,2).":".substr($lp[2],4,2);

}

# S/setup allows to re-arrange data in logfile view

if ($switches =~ /S/) {

# step 1: run 0 .. 10 to gather all data

for (my $i = 0; $i < 10; $i++) {

if ($setup{"field_${i}_array"}) {
my $j = 0;
foreach my $fp (split(/$setup{"field_${i}_array"}/,$lp[$i])) {
$setup{"field_${i}_array_$j"} = $fp;
$j++;
}
}

}

# step 2: run 0 .. 10 to process all fields

for (my $i = 0; $i < 10; $i++) {

if ($setup{"field_${i}_replace"}) {

$lp[$i] = $setup{"field_${i}_replace"};

my $whilecounter;
while ($lp[$i] =~ /###_ARRAY_(.*?)_(.*?)_###/ && $whilecounter < 64) {
my $x1 = $1;
my $x2 = $2;
$lp[$i] =~ s~###_ARRAY_${x1}_${x2}_###~$setup{"field_${x1}_array_${x2}"}~;
$whilecounter++;
}

}
if ($setup{"field_${i}_hide"}) {
$lp[$i] = "";
}

}

}

if ($setup{'message_replace'}) {
$lp[5] = $setup{'message_replace'};
}
if ($setup{'message_hide'}) {
$lp[5] = "";
}


if ($setup{'id_replace'}) {
$lp[6] = $setup{'id_replace'};
}
if ($setup{'id_hide'}) {
$lp[6] = "";
}

if ($switches =~ /A/) {

my @txts = (
"I have successfully completed the following exercise:",
);
if ($BLK{'lang'} eq "de") {
@txts = (
"Ich habe folgende Übung erfolgreich abgeschlossen:",
);
}
if ($BLK{'lang'} eq "lu") {
@txts = (
"Ech hu folgend Übung erfollegräich ofgeschloss:",
);
}
if ($BLK{'lang'} eq "fr") {
@txts = (
"J'ai complété avec succès l'exercice suivant:",
);
}

my ($RES,$RES2,$app_data_ref) = &app_details("",$lp[5],$lp[6],$BLK_ref);
my %app_data = %$app_data_ref;

if ($switches =~ /h/) {

my $CUSTOM_NAME;
if ($lp[6] =~ m~:custom_name=(.*?):~) {
$CUSTOM_NAME = "$1 - ";
}

my $IMG = "images/apps/$app_data{'app'}_64.png";
if (!ONO::IO->exists($IMG)) {
$IMG = "ono/osr/images/spacer/trans.gif";
}

$lp[5] = qq~$txts[0]
<table class="default_table mt10">
<tr class="vtop">
<td><a href="/$APPS_BASE/$app_data{'app'}/"><div class="block48 radius10"><img class="block48 radius5" src="/$IMG" alt=""></div></a></td>
<td class="pad10_2">
<div class="bold mb5">$CUSTOM_NAME###_APP_ID_$app_data{'app'}_###</div>
$RES
</td>
</tr>
</table>
~;

} else {

$lp[5] = "###_APP_ID_$app_data{'app'}_### • $RES2";

}

$lp[6] = "";

}

if ($switches =~ /h/) {

my $PROIMG = ONO::FW::User::Init->profileimage($lp[4],48);

if ($lp[5] =~ /page: (.*?),/) {
$lp[5] = qq~$BLK{'Page'} ###_PAGE_${1}_### $BLK{'saved'}~;
}

if ("$lp[1]:$lp[4]:$lp[5]" eq $last_used) {

$total--;

} else {

$counter++;

$last_used = "$lp[1]:$lp[4]:$lp[5]";

$LINES .= qq~<tr class="vtop">
<td class="">
<div class="mr10">
<a href="/users/$lp[4]/">$PROIMG</a>
</div>
</td>
<td class="w100" style="font-size:95%">
<div class="abs">
<img class="block16 rel" src="/ono/osr/images/arrows/nuvola/white32shadow/nav_previous.png" style="z-index:1;top:10px;right:15px" alt="">
</div>
<div class="box_white rel" style="line-height:120%">
<div class="cola fr ml10 mb5">$lp[0]</div>
<a href="/users/$lp[4]" class="bold">$lp[4]:</a> <span class="col6">$lp[5]</span>
</div>
</td>
</tr>
~;

}

} else {

$counter++;

my $BG = "paper";
my $BUT;

if ($switches !~ /X/) {

$lp[5] =~ s~^(save|delete|rename|comment):~$BLK{$1}: ~;

$lp[5] =~ s~: (\w\w\w\w\w\w\w\w\w\w),~: <a href="$link&logfile_filter_text=$1">$1</a>,~g;
$lp[5] =~ s~(\ |\[)(words|chars|project|page|object|type|size):~$1$BLK{$2}: ~g;

$lp[5] =~ s~\[~<span class="cola italic">\[ ~;
$lp[5] =~ s~\]~ \]</span>~;

if ($switches =~ /c/) {

$lp[5] =~ s~\((.*?)\)~<span class="col9">\($1\)</span>~g;

}

}

if ($switches =~ /M/) {

$lp[5] =~ s~\<(p)\>~~g;
$lp[5] =~ s~\<\/(p)\>~~g;
$lp[5] =~ s~\  ~~g;
$lp[5] =~ s~ ~ ~g;
$lp[5] =~ s~ ~ ~g;

my ($level,$words) = ONO::Lib::Data::Monitor->string($lp[5]);
if ($level) {
$BG = "yellow";
if ($level > 1) {
$BG = "red";
}
$lp[5] .= " (monitor: $level,$words)";
}

}

if ($switches =~ /B/) {
$BUT = $setup{'logline_buttons'};
$BUT =~ s~###_LOGFILE_USER_###~$lp[4]~g;
$BUT =~ s~###_LOGFILE_ID_###~$lp[6]~g;
$BUT = qq~<td class="pad10_2 br">$BUT</td>~;
}

$LINES .= qq~<tr class="row vtop bt bb bg_$BG">
<td class="pad10_2 br">$lp[1]</td>
<td class="pad10_2 br">$lp[2]</td>
<td class="pad10_2 br$hide_x">$lp[0]</td>
<td class="pad10_2 br$hide_x"><a href="$link&logfile_filter_user=$lp[4]">$lp[4]</a></td>
<td class="pad10_2 br">$lp[5]</td>
<td class="pad10_2 br$hide_x"><a href="$link&logfile_filter_id=$lp[6]">$lp[6]</a></td>
<td class="pad10_2 br"><a href="$link&logfile_filter_ip=$lp[3]">$lp[3]</a></td>
$BUT
</tr>
~;

}
}
}
}

if ($switches =~ /h/) {

$LOG .= qq~<table class="wide_table mb10">~;
$NAV_STYLE = qq~ style="padding-left:68px"~;

} else {

my $ID_NAME = "ID";
if ($vars{'ono_toolbox_logfilereader_topbar_id'}) {
$ID_NAME = $vars{'ono_toolbox_logfilereader_topbar_id'};
}

my $BUT;
if ($switches =~ /B/) {
$BUT = qq~ <td></td>~;
}

$LOG .= qq~<table class="wide_table bt$BOPT bb$BOPT bl$BOPT br$BOPT">
<tr class="bg_title bt$BOPT bb$BOPT">
<td class="pad10_2">$BLK{'date'}</td>
<td class="pad10_2">$BLK{'time'}</td>
<td class="pad10_2$hide_x"></td>
<td class="pad10_2$hide_x">$BLK{'user'}</td>
<td class="pad10_2 w100">$BLK{'logfile'} - $BLK{'message'}</td>
<td class="pad10_2$hide_x">$ID_NAME</td>
<td class="pad10_2">IP</td>
$BUT
</tr>
~;

}

$LOG .= $LINES;

if ($switches !~ /(F|h)/) {

my $BUT;
if ($switches =~ /B/) {
$BUT = qq~ <td></td>~;
}

$LOG .= qq~ <tr class="bt bb bg_paper">
<td class="pad5_2"><input type="text" name="logfile_filter_date" value="$vars{'logfile_filter_date'}" class="w98"></td>
<td class="pad5_2"><input type="text" name="logfile_filter_time" value="$vars{'logfile_filter_time'}" class="w98"></td>
<td class="pad5_2$hide_x"></td>
<td class="pad5_2$hide_x"><input type="text" name="logfile_filter_user" value="$vars{'logfile_filter_user'}" class="w98"></td>
<td class="pad5_2 w100"><input type="text" name="logfile_filter_text" value="$vars{'logfile_filter_text'}" class="w100"></td>
<td class="pad5_2$hide_x"><input type="text" name="logfile_filter_id" value="$vars{'logfile_filter_id'}" class="w98"></td>
<td class="pad5_2"><input type="text" name="logfile_filter_ip" value="$vars{'logfile_filter_ip'}" class="w98"></td>
$BUT
</tr>
~;

$BUTTONS .= qq~ <td>
<input type="submit" name="logfile_filter" value="$BLK{'search'} / $BLK{'filter'}" class="button_green">
</td>
~;
}

if ($switches !~ /h/) {

$BUTTONS .= qq~ <td><a href="$link" class="button_yellow">$BLK{'show_all'}</a></td>~;

}

$LOG .= qq~ </table>~;

if ($switches =~ /h/) {
$LOG .= qq~ <div class="bg_fabric p2"></div>~;
} else {
$LOG .= qq~ <div class="bg_paper p2"></div>~;
}

if (ONO::IO->devstation) {
$LOG .= qq~<div class="bg_paper bt bb">[dev] file: $file</div>~;
}

$LOG .= qq~<div class="$NAV_STYLE">
<table class="wide_table">
<tr>
<td class="p0 w100"$NAV_STYLE>
~.ONO::Lib::UI::Page->nav(
1,
$vars{'page'},
1+int(($total-1)/$num),
"$link_nav=",
"",
"p",
).qq~
</td>
$BUTTONS
</tr>
</table>
</div>
~;

if ($switches !~ /F/) {
$LOG = "<$form>$LOG</form>";
}

if ($switches =~ /h/) {
if ($switches =~ /f/) {
$LOG = qq~<div class="box_fabric flat_top">$LOG</div>~;
} else {
$LOG = qq~<div class="box_fabric">$LOG</div>~;
}
}

if ($switches =~ /R/) {

$LOG = qq~<div class="box_cutter_black">$LOG</div>~;

}

return $LOG;

}

###############################################################################
# APACHE LOG
###############################################################################

sub apache_log {

my (
$self,
$switches,
) = @_;

#: This is probably DEPRECATED as of 2025.
#:
#: -c count accesses per minute, hour, day

my ($res1,%res2);

my @lines = ONO::IO->sys("access_log");

my @month_names = ONO::Lib::DateTime::ToolBox->month_name("","3au");
my %month_num;
for (my $m = 1; $m < 13; $m++) {
$month_num{$month_names[$m]} = $m;
}

foreach my $line (@lines) {

if ($switches =~ /c/) {

if ($line =~ m~^(.*)\[(.*?)/(.*?)/(.*?):(.*?):(.*?):(.*?)\]~) {

my $day = $2;
my $month = $month_num{$3};
my $year = $4;

my $hour = $5;
my $min = $6;

if (length $month < 2) {
$month = "0$month";
}

$res2{"$year$month$day"}++;
$res2{"$year$month$day-$hour"}++;
$res2{"$year$month$day-$hour$min"}++;

}
}

}

return ($res1,\%res2);

}

sub apache_log_stat {

#: This is probably DEPRECATED as of 2025.

my (
$self,
$year,
$mon,
$mday,
$hour,
$min,
$sec,
) = @_;

my ($res1,$res2_ref) = &apache_log("","c");
my %res2 = %$res2_ref;

my ($today_per_min,$today_per_sec,$hour_per_min,$hour_per_sec,$min_per_sec) = (0,0,0,0,0);

foreach my $opt ("$year$mon$mday","$year$mon$mday-$hour","$year$mon$mday-$hour$min") {
if (!$res2{$opt}) {
$res2{$opt} = 0;
}
}

if ($res2{"$year$mon$mday-$hour$min"}) {
$min_per_sec = int($res2{"$year$mon$mday-$hour$min"}/$sec*100)/100;
}
if ($res2{"$year$mon$mday-$hour"}) {
$hour_per_min = int($res2{"$year$mon$mday-$hour"}/$min*100)/100;
$hour_per_sec = int($res2{"$year$mon$mday-$hour"}/$min*100/60)/100;
}
if ($res2{"$year$mon$mday"}) {
$today_per_min = int($res2{"$year$mon$mday"}/($hour*60+$min)*100)/100;
$today_per_sec = int($res2{"$year$mon$mday"}/($hour*60+$min)*100/60)/100;
}

return (
$res2{"$year$mon$mday"},$res2{"$year$mon$mday-$hour"},$res2{"$year$mon$mday-$hour$min"},
$today_per_min,$today_per_sec,$hour_per_min,$hour_per_sec,$min_per_sec,
);

}

sub app_details {

#: This helps to read logfiles generated e-Learning platform applications.

my (
$self,
$lp5,
$lp6,
$BLK_ref,
) = @_;

my %BLK = %$BLK_ref;

my (%app_input,%app_data,$RES,$RES2);

foreach my $dat (split(/:/,$lp5)) {
my @dp = split(/=/,$dat);
$app_input{$dp[0]} = $dp[1];
}
foreach my $dat (split(/:/,$lp6)) {
my @dp = split(/=/,$dat);
$app_data{$dp[0]} = $dp[1];
}
foreach my $opt ('Questions:questions:green','Mistakes:mistakes:lightred','Hints:hints:lightred','Time:timer_time:green') {
my @op = split(/:/,$opt);
if ($app_data{"output_web_$op[1]"}) {
if ($op[1] eq "timer_time") {
$app_data{"output_web_$op[1]"} =~ s~\.~\:~g;
}
$RES .= qq~$BLK{$op[0]}: <span class="bold $op[2]">$app_data{"output_web_$op[1]"}</span> • ~;
$RES2 .= qq~$BLK{$op[0]}: $app_data{"output_web_$op[1]"} • ~;
}
}

$RES =~ s~ \&bull\; $~~;
$RES2 =~ s~ \&bull\; $~~;

return ($RES,$RES2,\%app_data);

}

###############################################################################
# end of script
###############################################################################

1;

__END__