#! /usr/bin/perl -wT

#----------------------------------------------------------------------
# heading     : Administration
# description : View log files
# navigation  : 4000 4400
#
# $Id: viewlogfiles,v 1.31 2005/08/25 21:08:41 charlieb Exp $
#----------------------------------------------------------------------
# copyright (C) 1999-2007 Mitel Networks Corporation
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
# 
#----------------------------------------------------------------------
use strict;
use esmith::FormMagick;

use esmith::ConfigDB;
use esmith::cgi;
use Time::TAI64;
use File::Basename;
use HTML::Entities;
use CGI;

use constant TRUE  => 1;
use constant FALSE => 0;

my $viewlogfiles = esmith::ConfigDB->open->get('viewlogfiles');

my $fm = esmith::FormMagick->new();

my $q = CGI->new();

# Save this operation preference for later.
my $operation = $q->param('operation');
$viewlogfiles->merge_props('DefaultOperation', $operation) 
    if $operation;

if ($operation and $operation eq 'download')
{
    perform_download($fm, $q);
}
else
{
    #$fm->debug(1);
    $fm->display();
}

=pod

=head1 NAME

viewlogfiles -- An interface to system log files, allowing for pattern
filtering and highlighting.

=head2 DESCRIPTION

This screen allows the administrator to flexibly view various system log files.

=begin testing

use esmith::FormMagick::Tester;
use esmith::TestUtils;
use esmith::ConfigDB;
use esmith::AccountsDB;

my $panel = $Original_File;
my $ua = esmith::FormMagick::Tester->new();

my $c = esmith::ConfigDB->open();
my $a = esmith::AccountsDB->open();

is  (mode($panel), '4755',              "Check permissions on script");
ok  ($ua->get_panel($panel),            "ABOUT TO RUN L10N TESTS");
is  ($ua->{status}, 200,                "200 OK");
ok  ($ua->set_language("en-us"),        "Set language to English");
ok  ($ua->get_panel($panel),            "Get panel");
is  ($ua->{status}, 200,                "200 OK");
like($ua->{content}, qr/View log files/, "Saw translated form title");

# View the messages log:

$ua->field("filename" => "messages");
$ua->field(highlightPattern => "");
$ua->field(matchPattern => "");
ok  ($ua->click("View log file"),       "Click View log file");
is  ($ua->{status}, 200,                "200 OK");
like($ua->{content}, qr/Viewed at/, "Saw validation messages");

# View the messages log and filter all output:

ok  ($ua->get_panel($panel),            "Get panel");
$ua->field("filename" => "messages");
$ua->field(highlightPattern => "gibberish-gibberish");
$ua->field(matchPattern => "gibberish-gibberish");
ok  ($ua->click("View log file"),       "Click View log file");
is  ($ua->{status}, 200,                "200 OK");
like($ua->{content}, qr/No matching lines/, "Saw validation messages");

=end testing

=cut

#------------------------------------------------------------
# subroutine to display initial form
#------------------------------------------------------------

our %logfiles = ();

sub timestamp2local
{
    $_ = shift;
    if (/^(\@[0-9a-f]{24})(.*)/s)
    {
	return Time::TAI64::tai64nlocal($1) . $2;
    }
    elsif (/^([0-9]{10}\.[0-9]{3})(.*)/s)
    {
	return localtime($1) . $2;
    }
    return $_;
}

sub findlogFiles
{
    my $fm = shift;
    my $q = $fm->{cgi};

    use File::Find;
    sub findlogfiles
    {
        my $path = $File::Find::name;

        if (-f)
        {
            # Remove leading /var/log/messages
            $path =~ s:^/var/log/::;
            # don't bother to collect files known to be non-text
            # or not log files
            foreach (qw(
                lastlog
                btmp$
                wtmp
                lock
                (?<!qpsmtpd/)state
                httpd/ssl_mutex.\d*
                httpd/ssl_scache.pag
                httpd/ssl_scache.dir
                \/config$
            ))
            {
                return if $path =~ /$_/;
            }

            my ($file_base, $file_path, $file_type) = fileparse($path);

            if ( $file_base =~ /@.*/ )
            {
                $logfiles{$path} = $file_path . timestamp2local($file_base);
            }
            else
            {
                $logfiles{$path} = $path;
            }
        }
    }

    # Now go and find all the files under /var/log
    find({wanted => \&findlogfiles, no_chdir => 1}, '/var/log');

    return \%logfiles;
}

#------------------------------------------------------------
# subroutine to perform actions and display result
#------------------------------------------------------------

sub performAndShowResult ($)
{
    my $fm = shift;
    my $q = $fm->{cgi};

    #------------------------------------------------------------
    # Verify the arguments and untaint the variables (see Camel
    # book, "Detecting and laundering tainted data", pg. 358)
    #------------------------------------------------------------
    my $filename = $q->param ('filename');
    if ($filename =~ /^([\S\s]+)$/)
    {
        $filename = $1;
    }
    elsif ($filename =~ /^$/)
    {
        $filename = "messages";
    }
    else
    {
        print $fm->localise("FILENAME_ERROR", { filename => "$filename" } );
        return;
    }
    my $matchPattern = $q->param ('matchPattern');
    if ($matchPattern =~ /^(\S+)$/)
    {
        $matchPattern = $1;
    }
    else
    {
        $matchPattern = ".";
    }
    my $highlightPattern = $q->param ('highlightPattern');
    if ($highlightPattern =~ /^(\S+)$/)
    {
        $highlightPattern = $1;
    }
    else
    {
        $highlightPattern = '';
    }

    #------------------------------------------------------------
    # Looks good; go ahead and generate the report.
    #------------------------------------------------------------

    my $fullpath = "/var/log/$filename";

    if (-z $fullpath)
    {
        print $fm->localise("LOG_FILE_EMPTY", { filename => "$filename" } );
        return;
    }

    print "$fullpath: \n";

    print $fm->localise("VIEWING_TIME", 
    { time => $fm->gen_locale_date_string() } );

    unless ( $matchPattern eq '.' )
    {
        print "<p>\n";
        print $fm->localise("MATCH_HEADER", { matchPattern => "$matchPattern" } );
    }

    if ( $highlightPattern )
    {
        print "<p>\n";
        print $fm->localise("HIGHLIGHT_HEADER", { highlightPattern => "$highlightPattern" } );
    }

    if ($filename =~ /\.gz$/)
    {
        my $pid = open(LOGFILE, "-|");
        die "Couldn't fork: $!" unless defined $pid;
        unless ($pid)
        {
            # Child
            exec("/bin/zcat", $fullpath)
            || die "Can't exec zcat: $!";
            # NOTREACHED
        }
    }
    else
    {
        open(LOGFILE, "$fullpath");
    }

    my $somethingMatched = 0;
    my $fileEmpty = 1;
    print "<PRE>";
    while(<LOGFILE>)
    {
        $fileEmpty = 0;
        next unless /$matchPattern/;
        $somethingMatched = 1;

        $_ = timestamp2local($_);
        $_ = HTML::Entities::encode_entities($_);
        ($highlightPattern && /$highlightPattern/)
        ? print "<b>$_</b>"
        : print;
    }
    print "</PRE>";

    if ($fileEmpty)
    {
        print "<p>\n";
        print $fm->localise("LOG_FILE_EMPTY");
    }
    else
    {
        unless ($somethingMatched)
        {
            print "<p>\n";
            print $fm->localise("NO_MATCHING_LINES");
        }
    }

    close LOGFILE;
    print $q->table({-width => '100%'}, 
                    $q->Tr($q->th({-class => 'sme-layout'},
                    $q->a( { -href => "viewlogfiles?page=0&Next=viewLog" .
                                      "&filename=$filename&matchPattern=$matchPattern" .
                                      "&highlightPattern=$highlightPattern" .
                                      "&operation=view",
                             -class => 'button-like'},
                                      $fm->localise('REFRESH')))));
    return;
}

sub print_viewlog_buttons
{
    my $self = shift;
    my $q = $self->{cgi};
    my $filename = $q->param('filename');
    my $matchPattern = $q->param('matchPattern');
    my $highlightPattern = $q->param('highlightPattern');

    print $q->table({-width => '100%'},
                    $q->Tr({-valign => 'center'}, 
                    $q->th({-class => 'sme-layout', -valign => 'center'},
                    $q->a( { -href => "viewlogfiles?page=0&Next=viewLog" .
                                      "&filename=$filename&matchPattern=$matchPattern" .
                                      "&highlightPattern=$highlightPattern" .
                                      "&skip_header=1",
                             -class => 'button-like'},
                                      $self->localise('DOWNLOAD')),
                    $q->submit( {-value => $self->localise('VIEW') } ))));
    return undef;
}

sub perform_download
{
    my $fm = shift;
    my $q = shift;
    my $filename = $q->param("filename");
    my $fullpath = "/var/log/$filename";

    # Save this information for later.
    my $operation = $q->param('operation');
    $viewlogfiles->merge_props('DefaultOperation', $operation);

    # If the client is on windows, we must handle this a little differently.
    my $win32 = FALSE;
    my $mac   = FALSE;
    my $agent = $ENV{HTTP_USER_AGENT} || "";
    if ($agent =~ /win32|windows/i)
    {
        $win32 = TRUE;
    }
    elsif ($agent =~ /mac/i)
    {
        $mac = TRUE;
    }
    
    # Check for errors first. Once we start sending the file it's too late to
    # report them.
    my $error = "";
    unless (-f $fullpath)
    {
        $error = $fm->localise("ERR_NOEXIST_FILE");
    }
    local *FILE;
    open(FILE, "<$fullpath")
        or $error = $fm->localise("ERR_NOOPEN_FILE");
    # Put other error checking here. 
    if ($error)
    {
        # FIXME: Add the header and footer template references here.
        print <<"EOF";
Content-Type: text/html

$error
EOF
        return undef;
    }
    # Fix the filename, as it might have a directory prefixed to it. 
    if ($filename =~ m#/#)
    {
        $filename = (split /\//, $filename)[-1];
    }

    # Otherwise, send the file. Start with the headers.
    # Note: The Content-disposition must be attachment, or IE will view the
    # file inline like it's told. It ignores the Content-type, but it likes
    # the Content-disposition (an officially unsupported header) for some
    # reason. Yay Microsoft.
    print <<"EOF";
Expires: 0
Content-type: application/octet-stream
Content-disposition: attachment; filename=$filename

EOF
    # And send the file.
    my $nl = "\n";
    if ($win32)  { $nl = "\r\n" }
    elsif ($mac) { $nl = "\r" }
    while (my $line = <FILE>)
    {
        chomp $line;
        print timestamp2local($line) . $nl;
    }
    close(FILE);

    return undef;
}

sub show_operation_widget
{
    my $self = shift;
    my $q = $self->{cgi};
    my $description = $self->localise('OP_DESC');
    my $label = $self->localise('OP_LABEL');

    my $defaultop = $viewlogfiles->prop('DefaultOperation');
    my $select = '<select name="operation" size="1">' . "\n";
    if ($defaultop eq 'view')
    {
        $select .= '<option value="view" selected>' .
                   $self->localise("VIEW") . '</option>' . "\n";
        $select .= '<option value="download">' .
                   $self->localise("DOWNLOAD") . '</option>' . "\n";
    }
    else
    {
        $select .= '<option value="view">' .
                   $self->localise("VIEW") . '</option>' . "\n";
        $select .= '<option value="download" selected>' .
                   $self->localise("DOWNLOAD") . '</option>' . "\n";
    }
    $select .= '</select>' . "\n";

    print $q->Tr($q->td({-colspan => 2}, $description));
    print $q->Tr($q->td({-class => 'sme-noborders-label'}, $label),
                 $q->td({-class => 'sme-noborders-content'}, $select));
                                    

    return undef;
}

__DATA__
<form title="View log files" header="/etc/e-smith/web/common/head.tmpl" footer="/etc/e-smith/web/common/foot.tmpl">
    <page name="Initial"
	pre-event="print_status_message()">
        <description>FIRSTPAGE_DESC</description>
        <field type="select" id="filename" options="findlogFiles()" 
            value="messages">
            <label>LOG_FILE_SELECT_DESC</label>
        </field>
        <field type="text" id="matchPattern">
            <description>FILTER_PATTERN_DESC</description>
            <label>FILTER_PATTERN_LABEL</label>
        </field>
        <field type="text" id="highlightPattern">
            <description>MATCH_PATTERN_DESC</description>
            <label>MATCH_PATTERN_LABEL</label>
        </field>
        <subroutine src="show_operation_widget()" />
        <field type="literal">
            <description>END_DESC</description>
        </field>
        <subroutine src="print_button('NEXT')" />
    </page>
    <page name="viewLog" pre-event="turn_off_buttons()">
        <subroutine src="performAndShowResult()" />
    </page>
</form>
