5#include <QStandardPaths>
8#include <QLoggingCategory>
9#include <QCoreApplication>
10#include <QJsonDocument>
13#include "HistoryDatabase.h"
15static Q_LOGGING_CATEGORY(log,
"WebBrowser.History.DB")
20static void sqlTrace(
void* ,
const char* sql) {
21 qDebug(log) <<
"SQL Trace:" << sql;
25void enableSqlTrace(
const QString& connectionName = QSqlDatabase::defaultConnection) {
26 QSqlDatabase db = QSqlDatabase::database(connectionName);
29 QVariant v = db.driver()->handle();
30 if (v.isValid() && qstrcmp(v.typeName(),
"sqlite3*") == 0) {
31 sqlite3* handle = *
static_cast<sqlite3**
>(v.data());
33 sqlite3_trace(handle, sqlTrace,
nullptr);
34 qDebug(log) <<
"SQLite trace enabled";
39void enableSqlTrace(
const QString& connectionName)
50 bool bRet = p->OpenSQLiteDatabase(
"history_connection", szPath);
60CHistoryDatabase::CHistoryDatabase(QObject *parent)
63 qDebug(log) << Q_FUNC_INFO;
66CHistoryDatabase::~CHistoryDatabase()
68 qDebug(log) << Q_FUNC_INFO;
71bool CHistoryDatabase::OnInitializeDatabase()
74 success = CDatabase::OnInitializeDatabase();
75 if(!success)
return false;
76 m_UrlDB.SetDatabase(GetDatabase());
77 m_UrlDB.OnInitializeDatabase();
81bool CHistoryDatabase::addHistoryEntry(
const QString &url)
83 if (url.isEmpty())
return false;
87 nUrlId = m_UrlDB.GetId(url);
89 nUrlId = m_UrlDB.AddUrl(url);
95 QSqlQuery query(GetDatabase());
97 "INSERT INTO history (url, visit_time) "
98 "VALUES (:url, :visit_time)"
100 query.bindValue(
":url", nUrlId);
101 QDateTime tm = QDateTime::currentDateTime();
102 query.bindValue(
":visit_time", tm);
104 bool success = query.exec();
106 qCritical(log) <<
"Failed to add history:" << query.lastError().text();
112bool CHistoryDatabase::addHistoryEntry(
const QString &url,
const QString& title,
const QDateTime& time)
114 if (url.isEmpty())
return false;
118 nUrlId = m_UrlDB.GetId(url);
120 nUrlId = m_UrlDB.AddUrl(url, title);
126 QSqlQuery query(GetDatabase());
128 "INSERT INTO history (url, visit_time) "
129 "VALUES (:url, :visit_time)"
131 query.bindValue(
":url", nUrlId);
132 query.bindValue(
":visit_time", time);
134 bool success = query.exec();
136 qCritical(log) <<
"Failed to add history:" << query.lastError().text();
142bool CHistoryDatabase::updateHistoryEntry(
const QString& url,
const QString &title,
const QIcon &icon)
144 if (url.isEmpty())
return false;
147 nUrlId = m_UrlDB.GetId(url);
149 nUrlId = m_UrlDB.AddUrl(url, title, icon);
152 return m_UrlDB.UpdateUrl(url, title, icon);
155bool CHistoryDatabase::updateHistoryEntry(
int id,
const QString &title,
const QIcon &icon)
157 QSqlQuery query(GetDatabase());
163 query.bindValue(
":id",
id);
164 if(query.exec() && query.next()) {
165 int urlId = query.value(0).toInt();
166 return m_UrlDB.UpdateUrl(urlId, title, icon);
172bool CHistoryDatabase::deleteHistoryEntry(
int id)
174 QSqlQuery query(GetDatabase());
175 query.prepare(
"DELETE FROM history WHERE id = :id");
176 query.bindValue(
":id",
id);
181bool CHistoryDatabase::deleteHistoryEntry(
const QString &url)
183 int urlId = m_UrlDB.GetId(url);
186 QSqlQuery query(GetDatabase());
187 query.prepare(
"DELETE FROM history WHERE url = :url");
188 query.bindValue(
":url", urlId);
192bool CHistoryDatabase::deleteDomainEntries(
const QString &szDomain)
194 if(szDomain.isEmpty())
return false;
195 auto domains = m_UrlDB.GetDomain(szDomain);
196 foreach(
auto urlId, domains) {
197 QSqlQuery query(GetDatabase());
198 query.prepare(
"DELETE FROM history WHERE url = :url");
199 query.bindValue(
":url", urlId);
201 qCritical(log) <<
"Failed to delete domain:" << szDomain <<
"url id:" << urlId;
202 m_UrlDB.DeleteUrl(urlId);
207bool CHistoryDatabase::clearHistory(
int days)
209 QSqlQuery query(GetDatabase());
212 QDateTime cutoff = QDateTime::currentDateTime().addDays(-days);
213 query.prepare(
"DELETE FROM history WHERE visit_time < :cutoff");
214 query.bindValue(
":cutoff", cutoff);
216 query.prepare(
"DELETE FROM history");
219 bool success = query.exec();
223 query.exec(
"VACUUM");
229void CHistoryDatabase::scheduleCleanup(
int maxDays,
int maxCount)
233 QSqlQuery query(GetDatabase());
234 QDateTime cutoff = QDateTime::currentDateTime().addDays(-maxDays);
235 query.prepare(
"DELETE FROM history WHERE visit_time < :cutoff");
236 query.bindValue(
":cutoff", cutoff);
242 QSqlQuery query(GetDatabase());
244 "DELETE FROM history WHERE id IN ("
245 " SELECT id FROM history "
246 " ORDER BY visit_time DESC "
247 " LIMIT -1 OFFSET :maxCount"
250 query.bindValue(
":maxCount", maxCount);
255QList<HistoryItem> CHistoryDatabase::getAllHistory(
int limit,
int offset)
257 QList<HistoryItem> historyList;
259 QSqlQuery query(GetDatabase());
262 "SELECT id, url, visit_time "
264 "ORDER BY visit_time DESC "
268 "SELECT id, url, visit_time "
270 "ORDER BY visit_time DESC "
271 "LIMIT :limit OFFSET :offset"
273 query.bindValue(
":limit", limit);
274 query.bindValue(
":offset", offset);
277 while (query.next()) {
279 item.id = query.value(0).toInt();
281 item.url = urlItem.szUrl;
282 item.title = urlItem.szTitle;
283 item.visitTime = query.value(2).toDateTime();
284 item.icon = urlItem.icon;
285 historyList.append(item);
288 qCritical(log) <<
"Failed to get all history:" << query.lastError().text();
294QList<HistoryItem> CHistoryDatabase::getHistoryByDate(
const QDate &date)
296 QList<HistoryItem> historyList;
298 QSqlQuery query(GetDatabase());
300 "SELECT id, url, visit_time "
302 "WHERE date(visit_time) = date(:date) "
303 "ORDER BY visit_time DESC"
305 query.bindValue(
":date", date.toString(
"yyyy-MM-dd"));
308 while (query.next()) {
310 item.id = query.value(0).toInt();
312 item.url = urlItem.szUrl;
313 item.title = urlItem.szTitle;
314 item.visitTime = query.value(2).toDateTime();
315 item.icon = urlItem.icon;
316 historyList.append(item);
323QList<HistoryItem> CHistoryDatabase::getHistoryByDate(
324 const QDate &start,
const QDate &end,
int limit)
326 QList<HistoryItem> historyList;
328 QSqlQuery query(GetDatabase());
330 "SELECT id, url, visit_time "
332 "WHERE date(visit_time) >= date(:start) AND date(visit_time) <= date(:end) "
333 "ORDER BY visit_time DESC "
336 query.bindValue(
":start", start.toString(
"yyyy-MM-dd"));
337 query.bindValue(
":end", end.toString(
"yyyy-MM-dd"));
338 query.bindValue(
":limit", limit);
341 while (query.next()) {
343 item.id = query.value(0).toInt();
345 item.url = urlItem.szUrl;
346 item.title = urlItem.szTitle;
347 item.visitTime = query.value(2).toDateTime();
348 item.icon = urlItem.icon;
349 historyList.append(item);
356QList<HistoryItem> CHistoryDatabase::searchHistory(
const QString &keyword)
358 auto items = m_UrlDB.Search(keyword);
359 QList<HistoryItem> retItems;
360 foreach (
auto i, items) {
363 item.title = i.szTitle;
370HistoryItem CHistoryDatabase::getHistoryByUrl(
const QString &url)
374 if(url.isEmpty())
return item;
375 auto urlItem = m_UrlDB.GetItem(url);
379 QSqlQuery query(GetDatabase());
381 "SELECT id, visit_time "
382 "FROM history WHERE url = :url"
384 query.bindValue(
":url", urlItem.id);
386 if (query.exec() && query.next()) {
387 item.id = query.value(0).toInt();
388 item.url = urlItem.szUrl;
389 item.title = urlItem.szTitle;
390 item.icon = urlItem.icon;
391 item.visitTime = query.value(1).toDateTime();
397HistoryItem CHistoryDatabase::getHistoryById(
int id)
401 QSqlQuery query(GetDatabase());
403 "SELECT id, url, visit_time "
404 "FROM history WHERE id = :id"
406 query.bindValue(
":id",
id);
408 if (query.exec() && query.next()) {
409 item.id = query.value(0).toInt();
410 int urlId = query.value(1).toInt();
411 auto urlItem = m_UrlDB.GetItem(urlId);
412 item.url = urlItem.szUrl;
413 item.title = urlItem.szTitle;
414 item.icon = urlItem.icon;
415 item.visitTime = query.value(2).toDateTime();
421int CHistoryDatabase::getHistoryCount()
423 QSqlQuery query(GetDatabase());
424 query.exec(
"SELECT COUNT(*) FROM history");
427 return query.value(0).toInt();
433QDateTime CHistoryDatabase::getLastVisitTime()
435 QSqlQuery query(GetDatabase());
436 query.exec(
"SELECT MAX(visit_time) FROM history");
439 return query.value(0).toDateTime();
445bool CHistoryDatabase::importFromJson(
const QString &filename)
447 QFile file(filename);
448 if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
453 doc = QJsonDocument::fromJson(file.readAll());
456 auto array = doc.array();
457 for(
auto it = array.begin(); it != array.end(); it++) {
458 auto o = it->toObject();
459 QString url = o[
"url"].toString();
460 QString title = o[
"title"].toString();
461 QDateTime time = QDateTime::fromString(o[
"visit_time"].toString());
462 qDebug(log) <<
"title:" << title <<
"url:" << url <<
"visit_time:" << time;
463 bool ok = addHistoryEntry(url, title, time);
465 qWarning(log) <<
"Failed to add history:" << o[
"title"].toString();
473bool CHistoryDatabase::exportToJson(
const QString &filename)
475 QFile file(filename);
476 if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
479 QTextStream out(&file);
480#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
481 out.setEncoding(QStringConverter::Utf8);
483 out.setCodec(
"UTF-8");
485 out.setGenerateByteOrderMark(
true);
489 auto items = getAllHistory();
490 foreach(
auto it, items) {
492 title.insert(
"title", it.title);
493 title.insert(
"url", it.url);
494 title.insert(
"visit_time", it.visitTime.toString());
504bool CHistoryDatabase::importFromCSV(
const QString &filename)
506 QFile file(filename);
507 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
508 qCritical(log) <<
"Failed to open file" << filename;
512 QTextStream in(&file);
513#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
514 in.setEncoding(QStringConverter::Utf8);
516 in.setCodec(
"UTF-8");
518 in.setGenerateByteOrderMark(
true);
519 int importedCount = 0;
523 GetDatabase().transaction();
526 while (!in.atEnd()) {
527 QString line = in.readLine().trimmed();
531 if (line.isEmpty() || line.startsWith(
"#")) {
536 if (lineNumber == 1) {
538 if (!validateCsvHeader(line)) {
539 throw QString(
"Invalid CSV header format");
545 QStringList fields = parseCsvLine(line);
547 if (fields.size() >= 3) {
548 if (importCsvRecord(fields)) {
552 qWarning() <<
"Invalid CSV line" << lineNumber <<
":" << line;
558 if (importedCount == 0) {
559 throw QString(
"No valid records found in CSV file");
562 if (!GetDatabase().commit()) {
563 throw QString(
"Failed to commit transaction: %1").arg(GetDatabase().lastError().text());
566 qDebug(log) <<
"Successfully imported" << importedCount <<
"records from CSV file";
568 }
catch (
const QString &error) {
569 GetDatabase().rollback();
571 qCritical(log) <<
"CSV import failed at line" << lineNumber <<
":" << error;
577bool CHistoryDatabase::exportToCSV(
const QString &filename)
579 QFile file(filename);
580 if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
583 QTextStream out(&file);
584#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
585 out.setEncoding(QStringConverter::Utf8);
587 out.setCodec(
"UTF-8");
589 out.setGenerateByteOrderMark(
true);
592 const QStringList headers = {
593 "Title",
"URL",
"Visit Time"
595 out << headers.join(
",") <<
"\n";
597 auto items = getAllHistory();
598 foreach(
auto it, items) {
600 row << escapeForCsv(it.title);
601 row << escapeForCsv(it.url);
602 row << it.visitTime.toString();
603 out << row.join(
",") <<
"\n";
610QString CHistoryDatabase::escapeForCsv(
const QString &text)
616 bool needQuotes = text.contains(
',') ||
617 text.contains(
'"') ||
618 text.contains(
'\n') ||
619 text.contains(
'\r') ||
620 text.contains(
'\t') ||
621 text.startsWith(
' ') ||
622 text.endsWith(
' ') ||
623 text.startsWith(
'\t') ||
630 QString escaped = text;
631 escaped.replace(
"\"",
"\"\"");
633 return "\"" + escaped +
"\"";
636QString CHistoryDatabase::unescapeCsvField(
const QString &field)
638 if (field.isEmpty()) {
642 QString unescaped = field;
645 if (unescaped.startsWith(
'"') && unescaped.endsWith(
'"')) {
646 unescaped = unescaped.mid(1, unescaped.length() - 2);
650 unescaped.replace(
"\"\"",
"\"");
652 return unescaped.trimmed();
655QStringList CHistoryDatabase::parseCsvLine(
const QString &line)
659 bool inQuotes =
false;
661 for (
int i = 0; i < line.length(); ++i) {
666 if (i + 1 < line.length() && line[i + 1] ==
'"') {
670 inQuotes = !inQuotes;
672 }
else if (ch ==
',' && !inQuotes) {
673 fields.append(field.trimmed());
681 if (!field.isEmpty()) {
682 fields.append(field.trimmed());
688bool CHistoryDatabase::validateCsvHeader(
const QString &headerLine)
690 QStringList headers = parseCsvLine(headerLine);
693 if (headers.size() < 3) {
698 QStringList requiredHeaders = {
"Title",
"URL",
"Visit Time"};
699 for (
const QString &required : requiredHeaders) {
700 if (!headers.contains(required, Qt::CaseInsensitive)) {
701 qWarning(log) <<
"Missing required header:" << required;
709bool CHistoryDatabase::importCsvRecord(
const QStringList &fields)
711 if (fields.size() < 3) {
721 item.title = fields[0];
723 item.url = fields[1];
726 item.visitTime = QDateTime::fromString(fields[2]);
727 if (!item.visitTime.isValid()) {
729 item.visitTime = QDateTime::fromString(fields[2]);
730 if (!item.visitTime.isValid()) {
731 item.visitTime = QDateTime::currentDateTime();
735 return addHistoryEntry(item.url, item.title, item.visitTime);
738bool CHistoryDatabase::ExportToJson(QJsonObject &obj)
743bool CHistoryDatabase::ImportFromJson(
const QJsonObject &obj)
748bool CHistoryDatabase::OnInitializeSqliteDatabase()
750 QSqlQuery query(GetDatabase());
753 bool success = query.exec(
754 "CREATE TABLE IF NOT EXISTS history ("
755 " id INTEGER PRIMARY KEY AUTOINCREMENT,"
756 " url INTEGER NOT NULL,"
757 " visit_time DATETIME DEFAULT CURRENT_TIMESTAMP"
762 qCritical(log) <<
"Failed to create table:" << query.lastError().text();
767 success = query.exec(
"CREATE INDEX IF NOT EXISTS idx_history_url ON history(url)");
769 qWarning(log) <<
"Failed to create index idx_history_url:"
770 << query.lastError().text()
771 << query.executedQuery();
773 success = query.exec(
"CREATE INDEX IF NOT EXISTS idx_history_time ON history(visit_time)");
775 qWarning(log) <<
"Failed to create index idx_history_time:"
776 << query.lastError().text()
777 << query.executedQuery();
783bool CHistoryDatabase::OnInitializeMySqlDatabase()
785 QSqlQuery query(GetDatabase());
788 bool success = query.exec(
789 "CREATE TABLE IF NOT EXISTS history ("
790 " id INTEGER PRIMARY KEY AUTO_INCREMENT,"
791 " url INTEGER NOT NULL,"
792 " visit_time DATETIME DEFAULT CURRENT_TIMESTAMP,"
793 " INDEX idx_history_url (url),"
794 " INDEX idx_history_time (visit_time)"
799 qCritical(log) <<
"Failed to create table:" << query.lastError().text();