summaryrefslogtreecommitdiffstats
path: root/src/collectiondb.cpp
diff options
context:
space:
mode:
authorMavridis Philippe <mavridisf@gmail.com>2021-01-13 19:26:24 +0200
committerMavridis Philippe <mavridisf@gmail.com>2021-01-13 19:26:24 +0200
commit8c20dc919f7d54eb48fb60f39ba5e1d466a70763 (patch)
tree44d89f278d5dd066603e5ab9c0b270bc8eb4ad51 /src/collectiondb.cpp
downloadklamav-8c20dc919f7d54eb48fb60f39ba5e1d466a70763.tar.gz
klamav-8c20dc919f7d54eb48fb60f39ba5e1d466a70763.zip
Initial commit
Signed-off-by: Mavridis Philippe <mavridisf@gmail.com>
Diffstat (limited to 'src/collectiondb.cpp')
-rw-r--r--src/collectiondb.cpp791
1 files changed, 791 insertions, 0 deletions
diff --git a/src/collectiondb.cpp b/src/collectiondb.cpp
new file mode 100644
index 0000000..ebb9742
--- /dev/null
+++ b/src/collectiondb.cpp
@@ -0,0 +1,791 @@
+// (c) 2004 Mark Kretschmann <markey@web.de>
+// (c) 2004 Christian Muehlhaeuser <chris@chris.de>
+// (c) 2004 Sami Nieminen <sami.nieminen@iki.fi>
+// (c) 2005 Ian Monroe <ian@monroe.nu>
+// See COPYING file for licensing information.
+
+
+#include "config.h"
+#include "klamavconfig.h"
+#include "collectiondb.h"
+#include "klamav.h"
+#include "activityviewer.h"
+
+
+#include "sqlite/sqlite3.h"
+
+
+#include <qfile.h>
+#include <qtimer.h>
+
+#include <kapplication.h>
+#include <kconfig.h>
+#include <kglobal.h>
+#include <kinputdialog.h> //setupCoverFetcher()
+#include <kio/job.h>
+#include <klineedit.h> //setupCoverFetcher()
+#include <klocale.h>
+#include <kmdcodec.h>
+#include <kstandarddirs.h>
+#include <kurl.h>
+#include <kdebug.h>
+#include <kio/netaccess.h>
+
+#include <cmath> //DbConnection::sqlite_power()
+#include <ctime> //query()
+#include <unistd.h> //usleep()
+
+
+
+
+
+//////////////////////////////////////////////////////////////////////////////////////////
+// CLASS CollectionDB
+//////////////////////////////////////////////////////////////////////////////////////////
+
+CollectionDB* CollectionDB::instance()
+{
+ static CollectionDB db;
+ return &db;
+}
+
+
+CollectionDB::CollectionDB( bool temporary )
+ : m_isTemporary( temporary )
+{
+
+ //<OPEN DATABASE>
+ initialize();
+ //</OPEN DATABASE>
+
+}
+
+CollectionDB::~CollectionDB()
+{
+
+ destroy();
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC
+//////////////////////////////////////////////////////////////////////////////////////////
+
+
+DbConnection
+*CollectionDB::getStaticDbConnection()
+{
+ return m_dbConnPool->getDbConnection();
+}
+
+
+void
+CollectionDB::returnStaticDbConnection( DbConnection *conn )
+{
+ m_dbConnPool->putDbConnection( conn );
+}
+
+
+/**
+ * Executes a SQL query on the already opened database
+ * @param statement SQL program to execute. Only one SQL statement is allowed.
+ * @return The queried data, or QStringList() on error.
+ */
+QStringList
+CollectionDB::query( const QString& statement, DbConnection *conn )
+{
+ clock_t start;
+ if ( DEBUGSQL )
+ {
+ kdDebug() << "Query-start: " << statement << endl;
+ start = clock();
+ }
+
+ DbConnection *dbConn;
+ if ( conn != NULL )
+ {
+ dbConn = conn;
+ }
+ else
+ {
+ dbConn = m_dbConnPool->getDbConnection();
+ }
+
+ //kdDebug() << statement << endl;
+
+ QStringList values = dbConn->query( statement );
+
+ //kdDebug() << values << endl;
+
+ if ( conn == NULL )
+ {
+ m_dbConnPool->putDbConnection( dbConn );
+ }
+
+ if ( DEBUGSQL )
+ {
+ clock_t finish = clock();
+ const double duration = (double) (finish - start) / CLOCKS_PER_SEC;
+ kdDebug() << "SQL-query (" << duration << "s): " << statement << endl;
+ }
+ return values;
+}
+
+
+/**
+ * Executes a SQL insert on the already opened database
+ * @param statement SQL statement to execute. Only one SQL statement is allowed.
+ * @return The rowid of the inserted item.
+ */
+int
+CollectionDB::insert( const QString& statement, const QString& table, DbConnection *conn )
+{
+ clock_t start;
+ if ( DEBUGSQL )
+ {
+ kdDebug() << "insert-start: " << statement << endl;
+ start = clock();
+ }
+
+ DbConnection *dbConn;
+ if ( conn != NULL )
+ {
+ dbConn = conn;
+ }
+ else
+ {
+ dbConn = m_dbConnPool->getDbConnection();
+ }
+
+ //kdDebug() << statement << endl;
+
+ int id = dbConn->insert( statement, table );
+
+ //kdDebug() << id << endl;
+
+ if ( conn == NULL )
+ {
+ m_dbConnPool->putDbConnection( dbConn );
+ }
+
+ if ( DEBUGSQL )
+ {
+ clock_t finish = clock();
+ const double duration = (double) (finish - start) / CLOCKS_PER_SEC;
+ kdDebug() << "SQL-insert (" << duration << "s): " << statement << endl;
+ }
+ return id;
+}
+
+
+bool
+CollectionDB::isEmpty()
+{
+ QStringList values;
+
+ values = query( "SELECT COUNT( type ) FROM klamav_activity LIMIT 0, 1;" );
+
+ return values.isEmpty() ? true : values.first() == "0";
+}
+
+
+bool
+CollectionDB::isValid(const QString &column, const QString &table)
+{
+ QStringList values1;
+ QStringList values2;
+
+ values1 = query( QString("SELECT COUNT( %1 ) FROM %2 LIMIT 0, 1;").arg(column).arg(table) );
+
+ //TODO? this returns true if value1 or value2 is not empty. Shouldn't this be and (&&)???
+ return !values1.isEmpty();
+}
+
+
+void
+CollectionDB::createTables( DbConnection */*conn*/ )
+{
+
+ //create tag table
+ createActivityTable();
+ //createMetaDBTable();
+
+ //create indexes
+/* query( QString( "CREATE INDEX date_idx%1 ON klamav_activity%2( date );" )
+ .arg( conn ? "_temp" : "" ).arg( conn ? "_temp" : "" ), conn );*/
+}
+
+void
+CollectionDB::createActivityTable( DbConnection *conn )
+{
+
+ query( "CREATE TABLE klamav_activity ( date TEXT, type TEXT, event TEXT, file TEXT);", conn );
+
+}
+
+void
+CollectionDB::createMetaDBTable( DbConnection *conn )
+{
+
+ query( "CREATE TABLE klamav_metadb ( id INTEGER PRIMARY KEY, date TEXT, submission INTEGER, creator TEXT, virus TEXT, alias TEXT, sender TEXT);", conn );
+ loadMetaDBTable(conn);
+
+}
+
+void
+CollectionDB::loadMetaDBTable( DbConnection */*conn*/ )
+{
+
+ QString location = locate("data", "klamav/about/metadb.txt");
+
+/* query( QString( ".import %1 klamav_activity;" ).arg(location), conn );
+
+ return;*/
+ QFile file( location );
+ if ( file.open( IO_ReadOnly ) ) {
+ QTextStream stream( &file );
+ QString line;
+ while ( !stream.atEnd() ) {
+ line = stream.readLine(); // line of text excluding '\n'
+ QStringList columns = QStringList::split("\t",line.remove("\""));
+ if (columns.count() >= 5)
+ insertMetaDBEntry(columns[0],columns[1],columns[2],columns[3],columns[4],columns[5]);
+ }
+ file.close();
+ }
+
+}
+void
+CollectionDB::dropTables( DbConnection *conn )
+{
+
+ query( QString( "DROP TABLE klamav_activity%1;" ).arg( conn ? "_temp" : "" ), conn );
+ query( QString( "DROP TABLE klamav_metadb%1;" ).arg( conn ? "_temp" : "" ), conn );
+}
+
+
+void
+CollectionDB::clearTables( DbConnection *conn )
+{
+
+ QString clearCommand = "DELETE FROM";
+
+ query( QString( "%1 klamav_activity%2;" ).arg( clearCommand ).arg( conn ? "_temp" : "" ), conn );
+ query( QString( "%1 klamav_metadb%2;" ).arg( clearCommand ).arg( conn ? "_temp" : "" ), conn );
+
+}
+
+
+void
+CollectionDB::moveTempTables( DbConnection *conn )
+{
+ insert( "INSERT INTO klamav_activity SELECT * FROM klamav_activity_temp;", NULL, conn );
+ insert( "INSERT INTO klamav_metadb SELECT * FROM klamav_activity_temp;", NULL, conn );
+
+}
+
+
+
+uint
+CollectionDB::IDFromValue( QString name, QString value, bool autocreate, const bool temporary, const bool updateSpelling, DbConnection *conn )
+{
+ if ( temporary )
+ name.append( "_temp" );
+ else
+ conn = NULL;
+
+ QStringList values =
+ query( QString(
+ "SELECT id, name FROM %1 WHERE name LIKE '%2';" )
+ .arg( name )
+ .arg( CollectionDB::instance()->escapeString( value ) ), conn );
+
+ if ( updateSpelling && !values.isEmpty() && ( values[1] != value ) )
+ {
+ query( QString( "UPDATE %1 SET id = %2, name = '%3' WHERE id = %4;" )
+ .arg( name )
+ .arg( values.first() )
+ .arg( CollectionDB::instance()->escapeString( value ) )
+ .arg( values.first() ), conn );
+ }
+
+ //check if item exists. if not, should we autocreate it?
+ uint id;
+ if ( values.isEmpty() && autocreate )
+ {
+ id = insert( QString( "INSERT INTO %1 ( name ) VALUES ( '%2' );" )
+ .arg( name )
+ .arg( CollectionDB::instance()->escapeString( value ) ), name, conn );
+
+ return id;
+ }
+
+ return values.isEmpty() ? 0 : values.first().toUInt();
+}
+
+
+QString
+CollectionDB::valueFromID( QString table, uint id )
+{
+ QStringList values =
+ query( QString(
+ "SELECT name FROM %1 WHERE id=%2;" )
+ .arg( table )
+ .arg( id ) );
+
+
+ return values.isEmpty() ? 0 : values.first();
+}
+
+
+QString
+CollectionDB::typeCount( const QString &type_id )
+{
+ QStringList values =
+ query( QString(
+ "SELECT COUNT( type ) FROM klamav_activity WHERE type = %1;" )
+ .arg( type_id ) );
+ return values.first();
+}
+
+
+QStringList
+CollectionDB::messagesForType( const QString &type_id, const bool isValue )
+{
+ if ( isValue)
+ {
+ return query( QString( "SELECT * FROM klamav_activity "
+ "WHERE (type = \"%1\" ) ;" )
+ .arg( type_id ) );
+ }
+ return "";
+}
+
+
+
+
+
+
+
+
+
+//////////////////////////////////////////////////////////////////////////////////////////
+// PRIVATE SLOTS
+//////////////////////////////////////////////////////////////////////////////////////////
+
+
+
+
+
+//////////////////////////////////////////////////////////////////////////////////////////
+// PRIVATE
+//////////////////////////////////////////////////////////////////////////////////////////
+
+void
+CollectionDB::initialize()
+{
+ m_dbConnPool = new DbConnectionPool( m_isTemporary );
+ DbConnection *dbConn = m_dbConnPool->getDbConnection();
+ m_dbConnPool->putDbConnection( dbConn );
+
+// KConfig* config = amaroK::config( "Collection Browser" );
+ if(!dbConn->isConnected())
+ kdDebug() << "db not connected" << endl;
+ //amaroK::MessageQueue::instance()->addMessage(dbConn->lastError());
+ if ( !dbConn->isInitialized() )
+ {
+ createTables();
+ }
+
+// if (!isValid("id","klamav_metadb"))
+// createMetaDBTable();
+
+ if (!isValid("type","klamav_activity"))
+ createActivityTable();
+
+
+ m_dbConnPool->createDbConnections();
+}
+
+
+void
+CollectionDB::destroy()
+{
+ delete m_dbConnPool;
+}
+
+
+
+
+//////////////////////////////////////////////////////////////////////////////////////////
+// CLASS DbConnection
+//////////////////////////////////////////////////////////////////////////////////////////
+
+DbConnection::DbConnection( DbConfig* config )
+ : m_config( config )
+{}
+
+
+DbConnection::~DbConnection()
+{}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////
+// CLASS SqliteConnection
+//////////////////////////////////////////////////////////////////////////////////////////
+
+SqliteConnection::SqliteConnection( SqliteConfig* config )
+ : DbConnection( config )
+{
+
+ QString homepath = getenv("HOME");
+ const QCString path = (homepath+"/.klamav/activity.db").local8Bit();
+
+
+ // Open database file and check for correctness
+ m_initialized = false;
+ QFile file( path );
+ if ( file.open( IO_ReadOnly ) )
+ {
+ QString format;
+ file.readLine( format, 50 );
+ if ( !format.startsWith( "SQLite format 3" ) )
+ {
+ kdDebug() << "Database versions incompatible. Removing and rebuilding database.\n";
+ }
+ else if ( sqlite3_open( path, &m_db ) != SQLITE_OK )
+ {
+ kdDebug() << "Database file corrupt. Removing and rebuilding database.\n";
+ sqlite3_close( m_db );
+ }
+ else
+ m_initialized = true;
+ }
+
+ if ( !m_initialized )
+ {
+ // Remove old db file; create new
+ QFile::remove( path );
+ if ( sqlite3_open( path, &m_db ) == SQLITE_OK )
+ {
+ m_initialized = true;
+ }
+ }
+ if ( m_initialized )
+ {
+ if( sqlite3_create_function(m_db, "rand", 0, SQLITE_UTF8, NULL, sqlite_rand, NULL, NULL) != SQLITE_OK )
+ m_initialized = false;
+ if( sqlite3_create_function(m_db, "power", 2, SQLITE_UTF8, NULL, sqlite_power, NULL, NULL) != SQLITE_OK )
+ m_initialized = false;
+ }
+
+ //optimization for speeding up SQLite
+ query( "PRAGMA default_synchronous = OFF;" );
+}
+
+
+SqliteConnection::~SqliteConnection()
+{
+ if ( m_db ) sqlite3_close( m_db );
+}
+
+
+QStringList SqliteConnection::query( const QString& statement )
+{
+ QStringList values;
+ int error;
+ const char* tail;
+ sqlite3_stmt* stmt;
+
+ //compile SQL program to virtual machine
+ error = sqlite3_prepare( m_db, statement.utf8(), statement.length(), &stmt, &tail );
+
+ if ( error != SQLITE_OK )
+ {
+ kdDebug() << k_funcinfo << " sqlite3_compile error:" << endl;
+ kdDebug() << sqlite3_errmsg( m_db ) << endl;
+ kdDebug() << "on query: " << statement << endl;
+ values = QStringList();
+ }
+ else
+ {
+ int busyCnt = 0;
+ int number = sqlite3_column_count( stmt );
+ //execute virtual machine by iterating over rows
+ while ( true )
+ {
+ error = sqlite3_step( stmt );
+
+ if ( error == SQLITE_BUSY )
+ {
+ if ( busyCnt++ > 20 ) {
+ kdDebug() << "Busy-counter has reached maximum. Aborting this sql statement!\n";
+ break;
+ }
+ ::usleep( 100000 ); // Sleep 100 msec
+ kdDebug() << "sqlite3_step: BUSY counter: " << busyCnt << endl;
+ }
+ if ( error == SQLITE_MISUSE )
+ kdDebug() << "sqlite3_step: MISUSE" << endl;
+ if ( error == SQLITE_DONE || error == SQLITE_ERROR )
+ break;
+
+ //iterate over columns
+ for ( int i = 0; i < number; i++ )
+ {
+ values << QString::fromUtf8( (const char*) sqlite3_column_text( stmt, i ) );
+ }
+ }
+ //deallocate vm ressources
+ sqlite3_finalize( stmt );
+
+ if ( error != SQLITE_DONE )
+ {
+ kdDebug() << k_funcinfo << "sqlite_step error.\n";
+ kdDebug() << sqlite3_errmsg( m_db ) << endl;
+ kdDebug() << "on query: " << statement << endl;
+ values = QStringList();
+ }
+ }
+
+ return values;
+}
+
+
+int SqliteConnection::insert( const QString& statement, const QString& /* table */ )
+{
+ int error;
+ const char* tail;
+ sqlite3_stmt* stmt;
+
+ //compile SQL program to virtual machine
+ error = sqlite3_prepare( m_db, statement.utf8(), statement.length(), &stmt, &tail );
+
+ if ( error != SQLITE_OK )
+ {
+ kdDebug() << k_funcinfo << " sqlite3_compile error:" << endl;
+ kdDebug() << sqlite3_errmsg( m_db ) << endl;
+ kdDebug() << "on insert: " << statement << endl;
+ }
+ else
+ {
+ int busyCnt = 0;
+ //execute virtual machine by iterating over rows
+ while ( true )
+ {
+ error = sqlite3_step( stmt );
+
+ if ( error == SQLITE_BUSY )
+ {
+ if ( busyCnt++ > 20 ) {
+ kdDebug() << "Busy-counter has reached maximum. Aborting this sql statement!\n";
+ break;
+ }
+ ::usleep( 100000 ); // Sleep 100 msec
+ kdDebug() << "sqlite3_step: BUSY counter: " << busyCnt << endl;
+ }
+ if ( error == SQLITE_MISUSE )
+ kdDebug() << "sqlite3_step: MISUSE" << endl;
+ if ( error == SQLITE_DONE || error == SQLITE_ERROR )
+ break;
+ }
+ //deallocate vm ressources
+ sqlite3_finalize( stmt );
+
+ if ( error != SQLITE_DONE )
+ {
+ kdDebug() << k_funcinfo << "sqlite_step error.\n";
+ kdDebug() << sqlite3_errmsg( m_db ) << endl;
+ kdDebug() << "on insert: " << statement << endl;
+ }
+ }
+ return sqlite3_last_insert_rowid( m_db );
+}
+
+
+// this implements a RAND() function compatible with the MySQL RAND() (0-param-form without seed)
+void SqliteConnection::sqlite_rand(sqlite3_context *context, int /*argc*/, sqlite3_value ** /*argv*/)
+{
+ sqlite3_result_double( context, static_cast<double>(KApplication::random()) / (RAND_MAX+1.0) );
+}
+
+// this implements a POWER() function compatible with the MySQL POWER()
+void SqliteConnection::sqlite_power(sqlite3_context *context, int argc, sqlite3_value **argv)
+{
+ Q_ASSERT( argc==2 );
+ if( sqlite3_value_type(argv[0])==SQLITE_NULL || sqlite3_value_type(argv[1])==SQLITE_NULL ) {
+ sqlite3_result_null(context);
+ return;
+ }
+ double a = sqlite3_value_double(argv[0]);
+ double b = sqlite3_value_double(argv[1]);
+ sqlite3_result_double( context, pow(a,b) );
+}
+
+
+
+
+//////////////////////////////////////////////////////////////////////////////////////////
+// CLASS SqliteConfig
+//////////////////////////////////////////////////////////////////////////////////////////
+
+SqliteConfig::SqliteConfig( const QString& dbfile )
+ : m_dbfile( dbfile )
+{
+}
+
+
+
+//////////////////////////////////////////////////////////////////////////////////////////
+// CLASS DbConnectionPool
+//////////////////////////////////////////////////////////////////////////////////////////
+
+DbConnectionPool::DbConnectionPool( bool temporary )
+ : m_isTemporary( temporary )
+ , m_semaphore( POOL_SIZE )
+{
+ m_dbConnType = DbConnection::sqlite;
+
+ m_semaphore += POOL_SIZE;
+ DbConnection *dbConn;
+
+ m_dbConfig = new SqliteConfig( "activity.db" );
+ dbConn = new SqliteConnection( static_cast<SqliteConfig*> ( m_dbConfig ) );
+
+ enqueue( dbConn );
+ m_semaphore--;
+ kdDebug() << "Available db connections: " << m_semaphore.available() << endl;
+}
+
+
+DbConnectionPool::~DbConnectionPool()
+{
+ m_semaphore += POOL_SIZE;
+ DbConnection *conn;
+ bool vacuum = !m_isTemporary;
+
+ while ( ( conn = dequeue() ) != 0 )
+ {
+ if ( m_dbConnType == DbConnection::sqlite && vacuum )
+ {
+ vacuum = false;
+ kdDebug() << "Running VACUUM" << endl;
+ conn->query( "VACUUM; ");
+ }
+
+ delete conn;
+ }
+
+ delete m_dbConfig;
+}
+
+
+void DbConnectionPool::createDbConnections()
+{
+ for ( int i = 0; i < POOL_SIZE - 1; i++ )
+ {
+ DbConnection *dbConn;
+
+ dbConn = new SqliteConnection( static_cast<SqliteConfig*> ( m_dbConfig ) );
+ enqueue( dbConn );
+ m_semaphore--;
+ }
+ kdDebug() << "Available db connections: " << m_semaphore.available() << endl;
+}
+
+
+DbConnection *DbConnectionPool::getDbConnection()
+{
+ m_semaphore++;
+ return dequeue();
+}
+
+
+void DbConnectionPool::putDbConnection( const DbConnection *conn )
+{
+ enqueue( conn );
+ m_semaphore--;
+}
+
+
+void
+CollectionDB::expireActivity(const QString &days )
+{
+ int intdays = days.toInt();
+ if (intdays > 0)
+ intdays--;
+
+ query( QString( "DELETE FROM klamav_activity WHERE date < datetime('now','localtime','-%2 days', 'start of day');" ).arg(intdays) );
+}
+
+
+QStringList
+CollectionDB::allActivity( )
+{
+ return query( QString( "SELECT * FROM klamav_activity" ) );
+}
+
+QStringList
+CollectionDB::allActivityOfType(const QString &type,const QString &days )
+{
+ int intdays = days.toInt();
+
+ intdays--;
+
+ if (type == "All Types")
+ return query( QString( "SELECT * FROM klamav_activity WHERE date > datetime('now','localtime','-%2 days', 'start of day');" ).arg(intdays) );
+
+ return query( QString( "SELECT * FROM klamav_activity where type = '%1'"
+ " and date > datetime('now','localtime','-%2 days', 'start of day');" ).arg(type).arg(intdays) );
+}
+
+void CollectionDB::insertEvent(const QString &type, const QString &event, const QString &file, DbConnection *conn)
+{
+
+ if (((!(KlamavConfig::launchShutdown())) && (type == "Launch")) ||
+ ((!(KlamavConfig::softwareUpdates())) && (type == "Updates")) ||
+ ((!(KlamavConfig::dBUpdates())) && (type == "Updates")) ||
+ ((!(KlamavConfig::quarantined())) && (type == "Quarantine")) ||
+ ((!(KlamavConfig::virusFound())) && (type == "Virus Found")) ||
+ ((!(KlamavConfig::error())) && (type == "Error Found")) ||
+ ((!(KlamavConfig::startedStoppedCancelled())) && (type == "Manual Scan")) ||
+ ((!(KlamavConfig::startedStoppedCancelled())) && (type == "Auto-Scan")))
+ return;
+
+ QString date = query( QString( "select datetime('now','localtime')" ) ).first();
+ insert( QString( "INSERT INTO klamav_activity ( type, date, event, file )"
+ " VALUES ( '%1', '%2', '%3', '%4' );" )
+ .arg( type )
+ .arg( date )
+ .arg( event )
+ .arg( file )
+ , "klamav_activity", conn);
+
+ kmain->activityviewer->insertItem(date,type,event,file);
+}
+
+void CollectionDB::insertMetaDBEntry(const QString &date, const QString &submission, const QString &creator,const QString &virus,const QString &alias, const QString &sender,DbConnection *conn)
+{
+
+ insert( QString( "INSERT INTO klamav_metadb ( id, date, submission, creator, virus, alias, sender )"
+ " VALUES ( NULL, \"%1\", \"%2\", \"%3\", \"%4\", \"%5\", \"%6\");" )
+ .arg( date )
+ .arg( submission.toInt() )
+ .arg( creator )
+ .arg( virus )
+ .arg( alias )
+ .arg( sender )
+ , "klamav_metadb", conn);
+
+}
+
+QString
+CollectionDB::latestMetaDBDate( )
+{
+ QStringList highest = query( QString( "SELECT MAX(date) FROM klamav_metadb;" ));
+
+ return highest.first();
+}
+
+
+#include "collectiondb.moc"