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()
73 QSqlQuery query(GetDatabase());
76 bool success = query.exec(
77 "CREATE TABLE IF NOT EXISTS history ("
78 " id INTEGER PRIMARY KEY AUTOINCREMENT,"
79 " url INTEGER NOT NULL,"
80 " visit_time DATETIME DEFAULT CURRENT_TIMESTAMP"
85 qCritical(log) <<
"Failed to create table:" << query.lastError().text();
90 query.exec(
"CREATE INDEX IF NOT EXISTS idx_history_url ON history(url)");
91 query.exec(
"CREATE INDEX IF NOT EXISTS idx_history_time ON history(visit_time)");
93 m_UrlDB.SetDatabase(GetDatabase());
94 m_UrlDB.OnInitializeDatabase();
98bool CHistoryDatabase::addHistoryEntry(
const QString &url)
100 if (url.isEmpty())
return false;
104 nUrlId = m_UrlDB.GetId(url);
106 nUrlId = m_UrlDB.AddUrl(url);
112 QSqlQuery query(GetDatabase());
114 "INSERT INTO history (url, visit_time) "
115 "VALUES (:url, :visit_time)"
117 query.bindValue(
":url", nUrlId);
118 QDateTime tm = QDateTime::currentDateTime();
119 query.bindValue(
":visit_time", tm);
121 bool success = query.exec();
123 qCritical(log) <<
"Failed to add history:" << query.lastError().text();
129bool CHistoryDatabase::addHistoryEntry(
const QString &url,
const QString& title,
const QDateTime& time)
131 if (url.isEmpty())
return false;
135 nUrlId = m_UrlDB.GetId(url);
137 nUrlId = m_UrlDB.AddUrl(url, title);
143 QSqlQuery query(GetDatabase());
145 "INSERT INTO history (url, visit_time) "
146 "VALUES (:url, :visit_time)"
148 query.bindValue(
":url", nUrlId);
149 query.bindValue(
":visit_time", time);
151 bool success = query.exec();
153 qCritical(log) <<
"Failed to add history:" << query.lastError().text();
159bool CHistoryDatabase::updateHistoryEntry(
const QString& url,
const QString &title,
const QIcon &icon)
161 if (url.isEmpty())
return false;
164 nUrlId = m_UrlDB.GetId(url);
166 nUrlId = m_UrlDB.AddUrl(url, title, icon);
169 return m_UrlDB.UpdateUrl(url, title, icon);
172bool CHistoryDatabase::updateHistoryEntry(
int id,
const QString &title,
const QIcon &icon)
174 QSqlQuery query(GetDatabase());
180 query.bindValue(
":id",
id);
181 if(query.exec() && query.next()) {
182 int urlId = query.value(0).toInt();
183 return m_UrlDB.UpdateUrl(urlId, title, icon);
189bool CHistoryDatabase::deleteHistoryEntry(
int id)
191 QSqlQuery query(GetDatabase());
192 query.prepare(
"DELETE FROM history WHERE id = :id");
193 query.bindValue(
":id",
id);
198bool CHistoryDatabase::deleteHistoryEntry(
const QString &url)
200 int urlId = m_UrlDB.GetId(url);
203 QSqlQuery query(GetDatabase());
204 query.prepare(
"DELETE FROM history WHERE url = :url");
205 query.bindValue(
":url", urlId);
209bool CHistoryDatabase::deleteDomainEntries(
const QString &szDomain)
211 if(szDomain.isEmpty())
return false;
212 auto domains = m_UrlDB.GetDomain(szDomain);
213 foreach(
auto urlId, domains) {
214 QSqlQuery query(GetDatabase());
215 query.prepare(
"DELETE FROM history WHERE url = :url");
216 query.bindValue(
":url", urlId);
218 qCritical(log) <<
"Failed to delete domain:" << szDomain <<
"url id:" << urlId;
219 m_UrlDB.DeleteUrl(urlId);
224bool CHistoryDatabase::clearHistory(
int days)
226 QSqlQuery query(GetDatabase());
229 QDateTime cutoff = QDateTime::currentDateTime().addDays(-days);
230 query.prepare(
"DELETE FROM history WHERE visit_time < :cutoff");
231 query.bindValue(
":cutoff", cutoff);
233 query.prepare(
"DELETE FROM history");
236 bool success = query.exec();
240 query.exec(
"VACUUM");
246void CHistoryDatabase::scheduleCleanup(
int maxDays,
int maxCount)
250 QSqlQuery query(GetDatabase());
251 QDateTime cutoff = QDateTime::currentDateTime().addDays(-maxDays);
252 query.prepare(
"DELETE FROM history WHERE visit_time < :cutoff");
253 query.bindValue(
":cutoff", cutoff);
259 QSqlQuery query(GetDatabase());
261 "DELETE FROM history WHERE id IN ("
262 " SELECT id FROM history "
263 " ORDER BY visit_time DESC "
264 " LIMIT -1 OFFSET :maxCount"
267 query.bindValue(
":maxCount", maxCount);
272QList<HistoryItem> CHistoryDatabase::getAllHistory(
int limit,
int offset)
274 QList<HistoryItem> historyList;
276 QSqlQuery query(GetDatabase());
279 "SELECT id, url, visit_time "
281 "ORDER BY visit_time DESC "
285 "SELECT id, url, visit_time "
287 "ORDER BY visit_time DESC "
288 "LIMIT :limit OFFSET :offset"
290 query.bindValue(
":limit", limit);
291 query.bindValue(
":offset", offset);
294 while (query.next()) {
296 item.id = query.value(0).toInt();
298 item.url = urlItem.szUrl;
299 item.title = urlItem.szTitle;
300 item.visitTime = query.value(2).toDateTime();
301 item.icon = urlItem.icon;
302 historyList.append(item);
305 qCritical(log) <<
"Failed to get all history:" << query.lastError().text();
311QList<HistoryItem> CHistoryDatabase::getHistoryByDate(
const QDate &date)
313 QList<HistoryItem> historyList;
315 QSqlQuery query(GetDatabase());
317 "SELECT id, url, visit_time "
319 "WHERE date(visit_time) = date(:date) "
320 "ORDER BY visit_time DESC"
322 query.bindValue(
":date", date.toString(
"yyyy-MM-dd"));
325 while (query.next()) {
327 item.id = query.value(0).toInt();
329 item.url = urlItem.szUrl;
330 item.title = urlItem.szTitle;
331 item.visitTime = query.value(2).toDateTime();
332 item.icon = urlItem.icon;
333 historyList.append(item);
340QList<HistoryItem> CHistoryDatabase::getHistoryByDate(
341 const QDate &start,
const QDate &end,
int limit)
343 QList<HistoryItem> historyList;
345 QSqlQuery query(GetDatabase());
347 "SELECT id, url, visit_time "
349 "WHERE date(visit_time) >= date(:start) AND date(visit_time) <= date(:end) "
350 "ORDER BY visit_time DESC "
353 query.bindValue(
":start", start.toString(
"yyyy-MM-dd"));
354 query.bindValue(
":end", end.toString(
"yyyy-MM-dd"));
355 query.bindValue(
":limit", limit);
358 while (query.next()) {
360 item.id = query.value(0).toInt();
362 item.url = urlItem.szUrl;
363 item.title = urlItem.szTitle;
364 item.visitTime = query.value(2).toDateTime();
365 item.icon = urlItem.icon;
366 historyList.append(item);
373QList<HistoryItem> CHistoryDatabase::searchHistory(
const QString &keyword)
375 auto items = m_UrlDB.Search(keyword);
376 QList<HistoryItem> retItems;
377 foreach (
auto i, items) {
380 item.title = i.szTitle;
387HistoryItem CHistoryDatabase::getHistoryByUrl(
const QString &url)
391 if(url.isEmpty())
return item;
392 auto urlItem = m_UrlDB.GetItem(url);
396 QSqlQuery query(GetDatabase());
398 "SELECT id, visit_time "
399 "FROM history WHERE url = :url"
401 query.bindValue(
":url", urlItem.id);
403 if (query.exec() && query.next()) {
404 item.id = query.value(0).toInt();
405 item.url = urlItem.szUrl;
406 item.title = urlItem.szTitle;
407 item.icon = urlItem.icon;
408 item.visitTime = query.value(1).toDateTime();
414HistoryItem CHistoryDatabase::getHistoryById(
int id)
418 QSqlQuery query(GetDatabase());
420 "SELECT id, url, visit_time "
421 "FROM history WHERE id = :id"
423 query.bindValue(
":id",
id);
425 if (query.exec() && query.next()) {
426 item.id = query.value(0).toInt();
427 int urlId = query.value(1).toInt();
428 auto urlItem = m_UrlDB.GetItem(urlId);
429 item.url = urlItem.szUrl;
430 item.title = urlItem.szTitle;
431 item.icon = urlItem.icon;
432 item.visitTime = query.value(2).toDateTime();
438int CHistoryDatabase::getHistoryCount()
440 QSqlQuery query(GetDatabase());
441 query.exec(
"SELECT COUNT(*) FROM history");
444 return query.value(0).toInt();
450QDateTime CHistoryDatabase::getLastVisitTime()
452 QSqlQuery query(GetDatabase());
453 query.exec(
"SELECT MAX(visit_time) FROM history");
456 return query.value(0).toDateTime();
462bool CHistoryDatabase::importFromJson(
const QString &filename)
464 QFile file(filename);
465 if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
470 doc = QJsonDocument::fromJson(file.readAll());
473 auto array = doc.array();
474 for(
auto it = array.begin(); it != array.end(); it++) {
475 auto o = it->toObject();
476 QString url = o[
"url"].toString();
477 QString title = o[
"title"].toString();
478 QDateTime time = QDateTime::fromString(o[
"visit_time"].toString());
479 qDebug(log) <<
"title:" << title <<
"url:" << url <<
"visit_time:" << time;
480 bool ok = addHistoryEntry(url, title, time);
482 qWarning(log) <<
"Failed to add history:" << o[
"title"].toString();
490bool CHistoryDatabase::exportToJson(
const QString &filename)
492 QFile file(filename);
493 if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
496 QTextStream out(&file);
497#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
498 out.setEncoding(QStringConverter::Utf8);
500 out.setCodec(
"UTF-8");
502 out.setGenerateByteOrderMark(
true);
506 auto items = getAllHistory();
507 foreach(
auto it, items) {
509 title.insert(
"title", it.title);
510 title.insert(
"url", it.url);
511 title.insert(
"visit_time", it.visitTime.toString());
521bool CHistoryDatabase::importFromCSV(
const QString &filename)
523 QFile file(filename);
524 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
525 qCritical(log) <<
"Failed to open file" << filename;
529 QTextStream in(&file);
530#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
531 in.setEncoding(QStringConverter::Utf8);
533 in.setCodec(
"UTF-8");
535 in.setGenerateByteOrderMark(
true);
536 int importedCount = 0;
540 GetDatabase().transaction();
543 while (!in.atEnd()) {
544 QString line = in.readLine().trimmed();
548 if (line.isEmpty() || line.startsWith(
"#")) {
553 if (lineNumber == 1) {
555 if (!validateCsvHeader(line)) {
556 throw QString(
"Invalid CSV header format");
562 QStringList fields = parseCsvLine(line);
564 if (fields.size() >= 3) {
565 if (importCsvRecord(fields)) {
569 qWarning() <<
"Invalid CSV line" << lineNumber <<
":" << line;
575 if (importedCount == 0) {
576 throw QString(
"No valid records found in CSV file");
579 if (!GetDatabase().commit()) {
580 throw QString(
"Failed to commit transaction: %1").arg(GetDatabase().lastError().text());
583 qDebug(log) <<
"Successfully imported" << importedCount <<
"records from CSV file";
585 }
catch (
const QString &error) {
586 GetDatabase().rollback();
588 qCritical(log) <<
"CSV import failed at line" << lineNumber <<
":" << error;
594bool CHistoryDatabase::exportToCSV(
const QString &filename)
596 QFile file(filename);
597 if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
600 QTextStream out(&file);
601#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
602 out.setEncoding(QStringConverter::Utf8);
604 out.setCodec(
"UTF-8");
606 out.setGenerateByteOrderMark(
true);
609 const QStringList headers = {
610 "Title",
"URL",
"Visit Time"
612 out << headers.join(
",") <<
"\n";
614 auto items = getAllHistory();
615 foreach(
auto it, items) {
617 row << escapeForCsv(it.title);
618 row << escapeForCsv(it.url);
619 row << it.visitTime.toString();
620 out << row.join(
",") <<
"\n";
627QString CHistoryDatabase::escapeForCsv(
const QString &text)
633 bool needQuotes = text.contains(
',') ||
634 text.contains(
'"') ||
635 text.contains(
'\n') ||
636 text.contains(
'\r') ||
637 text.contains(
'\t') ||
638 text.startsWith(
' ') ||
639 text.endsWith(
' ') ||
640 text.startsWith(
'\t') ||
647 QString escaped = text;
648 escaped.replace(
"\"",
"\"\"");
650 return "\"" + escaped +
"\"";
653QString CHistoryDatabase::unescapeCsvField(
const QString &field)
655 if (field.isEmpty()) {
659 QString unescaped = field;
662 if (unescaped.startsWith(
'"') && unescaped.endsWith(
'"')) {
663 unescaped = unescaped.mid(1, unescaped.length() - 2);
667 unescaped.replace(
"\"\"",
"\"");
669 return unescaped.trimmed();
672QStringList CHistoryDatabase::parseCsvLine(
const QString &line)
676 bool inQuotes =
false;
678 for (
int i = 0; i < line.length(); ++i) {
683 if (i + 1 < line.length() && line[i + 1] ==
'"') {
687 inQuotes = !inQuotes;
689 }
else if (ch ==
',' && !inQuotes) {
690 fields.append(field.trimmed());
698 if (!field.isEmpty()) {
699 fields.append(field.trimmed());
705bool CHistoryDatabase::validateCsvHeader(
const QString &headerLine)
707 QStringList headers = parseCsvLine(headerLine);
710 if (headers.size() < 3) {
715 QStringList requiredHeaders = {
"Title",
"URL",
"Visit Time"};
716 for (
const QString &required : requiredHeaders) {
717 if (!headers.contains(required, Qt::CaseInsensitive)) {
718 qWarning(log) <<
"Missing required header:" << required;
726bool CHistoryDatabase::importCsvRecord(
const QStringList &fields)
728 if (fields.size() < 3) {
738 item.title = fields[0];
740 item.url = fields[1];
743 item.visitTime = QDateTime::fromString(fields[2]);
744 if (!item.visitTime.isValid()) {
746 item.visitTime = QDateTime::fromString(fields[2]);
747 if (!item.visitTime.isValid()) {
748 item.visitTime = QDateTime::currentDateTime();
752 return addHistoryEntry(item.url, item.title, item.visitTime);
755bool CHistoryDatabase::ExportToJson(QJsonObject &obj)
760bool CHistoryDatabase::ImportFromJson(
const QJsonObject &obj)