/*
 * KlamOnAcc class -- the non-graphical class which manages clamonacc.
 *
 * Copyright (C) 2021 Mavridis Philippe <mavridisf@gmail.com>
 *
 * Portions taken from freshklam.cpp and scanviewer.cpp
 */

/* TODO:
   - Implement a separate start/stop daemon process so that we don't need to
     ask for root privgileges every time we start or kill clamonacc
   - processOutput: [Initializing] and [Scanner ready] notifications
 */

#include "klamonacc.h"
#include "klamonacc_alert.h"
#include "klamav.h"
#include "klamavconfig.h"
#include "collectiondb.h"
#include "directorylist.h"

#include <tdeglobal.h>
#include <tdelocale.h>
#include <tdeconfig.h>
#include <tdetempfile.h>
#include <kprocess.h>
#include <tdemessagebox.h>
#include <ksystemtray.h>
#include <knotifyclient.h>

/* Required by quarantine() function */
#include <kiconloader.h>
#include <tdeio/netaccess.h>

KlamOnAcc::KlamOnAcc( TQWidget *parent, const char *name )
    : TQObject( parent, name )
{
    config = TDEGlobal::config();
    config->setGroup("OnAccess");

    // Initial state
    toggle( config->readBoolEntry("EnableOnAccess", false) );
}

KlamOnAcc::~KlamOnAcc()
{
}

TQString KlamOnAcc::tdesu(TQString command, TQString caption, bool terminal)
{
    TQString sucommand;

    if(terminal)
        sucommand = TQString("tdesu --caption \"%1\" --ignorebutton -t ").arg( caption );
    else
        sucommand = TQString("tdesu --caption \"%1\" --ignorebutton ").arg( caption );

    sucommand += "\"" + command + "\"";

    return sucommand;
}

TQString KlamOnAcc::startPrepare()
{
    // Determine and write configuration
    TQString daemonOpts;
    TQStringList daemonConfig;

    // Create a config file based on the default one
    TQFile defaultConfigFile( "/etc/clamav/clamd.conf" );

    if( defaultConfigFile.open(IO_ReadOnly) )
    {
        TQTextStream in_stream( &defaultConfigFile );
        TQString in_line;
        int in_line_n = 0;

        while(! in_stream.atEnd() )
        {
            daemonConfig += in_stream.readLine();
            ++in_line_n;
        }

        defaultConfigFile.close();
    }

    // Set up ClamOnAcc's config
    daemonConfig += "OnAccessPrevention yes";

    config->setGroup("OnAccess");

    if ( config->readBoolEntry("ExtraScanning", false) )
        daemonConfig += "OnAccessExtraScanning yes";

    daemonConfig += TQString("OnAccessMaxFileSize %1M").arg(
        config->readNumEntry("OnAccessMaxFile", 5)
    );

    daemonConfig += "OnAccessExcludeUname clamav";

    // Specify directories to watch
    TQStringList dirs = CollectionSetup::pruneSelectedDirs( config->readListEntry("Watchlist") );

    if (! dirs.count() ) {
        fatalError( i18n("Please select the directories you want to watch from the Options dialog.") );
        return TQString::null;
    }

    for ( TQStringList::Iterator it = dirs.begin(); it != dirs.end(); it++ )
        daemonConfig += TQString("OnAccessIncludePath %1").arg(*it);

    /* BUG: DOES NOT WORK (why do they have this option then?)
       "ERROR: ClamInotif: can't exclude '/home/user/.trinity'" */
    // if ( config->readBoolEntry("ExcludeConfDir", true) )
    //    daemonConfig += TQString("OnAccessExcludePath %1/.trinity").arg(getenv("HOME"));

    // Write the config
    KTempFile tf;

    if ( tf.status() != 0 ) {
        tf.close();

        fatalError( i18n("Could not create temporary configuration file for ClamOnAcc!") );
        return TQString::null;
    }

    TQString tempFileName = tf.name();

    TQTextStream &ts = *(tf.textStream());

    for ( TQStringList::Iterator it = daemonConfig.begin(); it != daemonConfig.end(); it++ )
        ts << (*it) << endl;

    tf.close();

    // Set up ClamOnAcc's command-line options
    daemonOpts += " --fdpass -v --stdout --foreground";
    daemonOpts += TQString(" --config-file=%1").arg(tempFileName);

    // Make the start command
    TQString command = "clamonacc";
    command += daemonOpts;

    return command;
}

void KlamOnAcc::startProcess( TQString command )
{
    childproc = new KShellProcess();
    *childproc << command;
    childproc->start(TDEProcess::NotifyOnExit, TDEProcess::Stdout);

    connect( childproc, TQ_SIGNAL(receivedStdout(TDEProcess*, char*, int)), TQ_SLOT(processOutput(TDEProcess*, char*, int)) );
    connect( childproc, TQ_SIGNAL(processExited(TDEProcess*)), TQ_SLOT(childExited()) );

    emit stateUpdated();
}

void KlamOnAcc::start()
{
    if( active || !enabled ) return;
    active = true;
    crashed = false;

    // Log this event
    CollectionDB::instance()->insertEvent("On-Access Scanner","Starting On-Access Scanner",0);

    startProcess( tdesu(startPrepare(), i18n("Start On-Access Scanner"), true) );
}

TQString KlamOnAcc::stopPrepare()
{
    disconnect( childproc, 0, 0, 0 );

    // It's like this until a proper start/stop daemon is implemented
    return TQString("killall -9 clamonacc");
}

void KlamOnAcc::stopProcess( TQString command )
{
    if( childproc->isRunning() ) {
        TDEProcess *terminator = new KShellProcess();
        *terminator << command;
        terminator->start();
    }

    emit stateUpdated();
}

void KlamOnAcc::stop()
{
    if( !active || !enabled ) return;
    active = false;

    // Log this event
    CollectionDB::instance()->insertEvent("On-Access Scanner","Stopping On-Access Scanner",0);

    stopProcess( tdesu(stopPrepare(), i18n("Stop On-Access Scanner")) );
}


void KlamOnAcc::restart()
{
    kdDebug() << "restart()" << endl;
    if( isActive() )
    {
        active = false;

        // We combine two commands here
        TQString command = stopPrepare() + TQString("; ") + startPrepare();
        kdDebug() << "restart(): " << command << endl;
        startProcess( tdesu(command, i18n("Restart On-Access Scanner"), true) );

        active = true;
    }
}

void KlamOnAcc::toggle( bool on ) {
    kdDebug() << "toggle()" << endl;

    if ( !on && isEnabled() )
        disable();
    else if ( on && !isEnabled() )
        enable();
}

void KlamOnAcc::childExited() {
    if(active) // died too early
        fatalError( i18n("ClamOnAcc has died unexpectedly. If you did not kill it yourself, please check your ClamAV installation.") );
}

void KlamOnAcc::fatalError(TQString descr)
{
    if( crashed ) return; // do not display further errors

    active = false;
    crashed = true;

    CollectionDB::instance()->insertEvent("On-Access Scanner","On-Access Scanner has died!",0);

    disable();

    KMessageBox::sorry(
        0,
        descr,
        i18n("Fatal Error")
    );
}

void KlamOnAcc::processOutput(TDEProcess*, char* buffer, int buffSize)
{

    TQString buff( buffer );
    buff = buff.mid( 0, buff.find("\n") ).stripWhiteSpace();

    kdDebug() << "KLAMONACC " << buff << endl;

    int pos;

    if( buff.find("Could not connect to clamd") != -1 )
    {
        fatalError( i18n("The ClamAV daemon is unavailable! Please ensure that it is running and try again.") );
        return;
    }
    else if( buff.find("ClamInotif: watching") != -1 )
    {
        // TODO: "initialization complete" notification
    }
    else if( (pos = buff.find("FOUND")) != -1 )
    {
        fname = buff.mid( 0, buff.find(":") );
        vname = buff.mid( buff.find(":")+2, pos );

        if( shownAlerts.find(fname) != shownAlerts.end() )
            return; // alert already shown for this file

        TQListViewItem *virusItem;
        alert = new KlamOnAccAlert();
        alert->setModal(false);
        alert->setActiveWindow();

        TQListViewItem *virus = new TQListViewItem( alert->VirusList, fname, vname, i18n("Loose") );
        virus->setPixmap( 0, SmallIcon("klamav_virus") );

        shownAlerts << fname;
        alert->exec();

        if( alert->result() == TQDialog::Accepted )
            quarantine();
    }
}

void KlamOnAcc::enable()
{
    kdDebug() << "% ENABLE()" << endl;

    config->setGroup("OnAccess");
    config->writeEntry("EnableOnAccess", true);
    config->sync();

    enabled = true;

    if(! isActive() ) start();

    emit stateUpdated();
}

void KlamOnAcc::disable()
{
    kdDebug() << "% DISABLE()"  << endl;
    if( isActive() ) stop();

    config->setGroup("OnAccess");
    config->writeEntry("EnableOnAccess", false);
    config->sync();

    enabled = false;

    emit stateUpdated();
}

void KlamOnAcc::quarantine()
{
    TQDate today = TQDate::currentDate();
    TQTime now = TQTime::currentTime();
    TQString suffix = TQString(":%1 %2")
            .arg(today.toString("ddd MMMM d yyyy"))
            .arg(now.toString("hh-mm-ss-zzz ap"));

    TQStringList QuarantineList;
    QuarantineList.append(fname+":"+vname+suffix);

    /* The following has been taken nearly verbatim from scanviewer.cpp */
    bool allQuarantined=true;
    config->setGroup("Kuarantine");
    TQStringList lastQuarLocations = config->readListEntry("KuarantineLocations");

    tdemain->_tray->setPixmap(KSystemTray::loadIcon("klamav_quarantining"));

    TQString quarloc;
    for (TQStringList::Iterator it = lastQuarLocations.begin(); it == lastQuarLocations.begin() ; it++){
            quarloc = *it;
    }
    TQStringList lastQuarItems = config->readListEntry(TQString("Items %1").arg(quarloc));

    for (TQStringList::Iterator it = QuarantineList.begin(); it != QuarantineList.end(); it++ ){
        if (lastQuarItems.contains(*it) != 0) {
            lastQuarItems.remove(*it);
        }
        TQString item2 = (*it).stripWhiteSpace();
        int fnameStartPoint = 0;
        int dtStartPoint = item2.findRev(":");
        int fnameEndPoint = item2.findRev(":", (signed int)-((item2.length() - dtStartPoint)+1));
        TQString fname = item2.mid(fnameStartPoint,(fnameEndPoint - fnameStartPoint));
        TQString itemName = item2.mid((fnameEndPoint+1),((dtStartPoint+1) - (fnameEndPoint+2)));
        TQString when = item2.mid((dtStartPoint+1),(item2.length() - (dtStartPoint+1)));
        if (!(fname.isEmpty())){
            TQStringList tokens = TQStringList::split ( "/", fname, false );
            TQString qname = tokens.last();
            qname.prepend("/");
            qname.prepend(quarloc);
            qname.append(":"+when);
            if (TDEIO::NetAccess::file_move(fname,qname)){
                if (lastQuarItems.contains(item2))
                    lastQuarItems.remove(item2);
                lastQuarItems.prepend(item2);
                (alert->VirusList->findItem(fname,0))->setText(2,"Quarantined");
                (alert->VirusList->findItem(fname,0))->setPixmap( 0, SmallIcon("klamav") );
                chmod(qname.ascii(),0400);
                CollectionDB::instance()->insertEvent("Quarantine",TQString("Quarantined"),fname);

            }else{
                KMessageBox::information (tdemain, i18n("<p>There was a problem quarantining <b>%1</b>. Check your diskspace, the permissions on your quarantine location and whether a file with the same name already exists in the quarantine. </p>").arg(fname));
            }


        }
    }

    emit stateUpdated();

    config->writeEntry(TQString("Items %1").arg(quarloc), lastQuarItems);
    config->sync();

}

#include "klamonacc.moc"