All checks were successful
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 34s
CI Code / Code Coverage (push) Successful in 2m36s
CI Code / Linux (debian) (push) Successful in 4m41s
CI Code / Linux (ubuntu) (push) Successful in 4m52s
CI Code / Linux (arch) (push) Successful in 5m40s
Sync with upstream profanity-im/profanity.
Major upstream changes incorporated:
Memory management
- Replace malloc+memset with g_new0 throughout codebase
- Adopt auto_gchar / auto_gcharv / auto_gerror / auto_jid cleanup macros
- Replace free() with g_free() for GAlloc'd memory
Editor rewrite
- Remove pthread-based async editor; use GChildWatch callback API
- New launch_editor(initial_content, callback, user_data) interface
- Proper signal handling (SIGINT, SIGTSTP, SIGPIPE reset in child)
- ui_suspend()/ui_resume() integration for TTY management
OMEMO improvements
- Dual backend support: libsignal-protocol-c and libomemo-c
- Proper pre-key removal after use (XEP-0384 compliance)
- Automatic pre-key regeneration when store drops below threshold
- New functions: omemo_is_device_active(), omemo_is_jid_trusted()
- omemo_get_jid_untrusted_fingerprints() for better error messages
- Fingerprint notifications on new device identity discovery
- Deterministic pre-key ID generation tracking max_pre_key_id
- omemo_trust_changed() UI updates on trust state changes
JID validation
- RFC 6122-compliant validation in jid_is_valid()
- Character-level checks (RFC 6122 forbidden chars: & ' / : < > @)
- Length limits: 1023 per component, 3071 total
- New jid_is_valid_user_jid() for user vs. service JID distinction
Database
- Schema migration v3: UNIQUE constraint on archive_id for deduplication
- Triggers for corrected message tracking (replaces_db_id / replaced_by_db_id)
- db_history_result_t return type, _truncate_datetime_suffix()
UI / console
- win_warn_needed() / win_warn_sent() warning deduplication hash table
- PAD_MIN_HEIGHT dynamic pad sizing with PAD_THRESHOLD auto-cleanup
- Spellcheck integration in input field with Unicode word detection
- cons_spellcheck_setting() for /settings ui output
- /[command]? shortcut for command help
Account config
- Account name sanitization for GKeyFile special chars ([ ] = # \n \r)
- Replace popen() with g_spawn_sync() for eval_password
- TLS policy: add "direct" option alongside legacy
Connection
- Port validation with g_assert (0–65535)
- SHA-256 certificate fingerprint support (XMPP_CERT_PUBKEY_FINGERPRINT_SHA256)
- "direct" TLS policy alias for legacy SSL
Common utilities
- str_xml_sanitize() for XML 1.0 illegal character removal
- string_matches_one_of() with formatted error messages
- valid_tls_policy_option() helper
- prof_date_time_format_iso8601() utility
- Improved strip_arg_quotes() with backslash unescaping
- prof_occurrences() uses g_slist_prepend + reverse for performance
PGP / OX
- Proper GPGME resource cleanup with goto-cleanup pattern
- g_string_free(xmppuri) leak fix in _ox_key_lookup
CSV export
- Use GString + g_file_set_contents instead of raw write() syscalls
Tests
- Restructured into subdirectories: command/, config/, xmpp/, ui/, omemo/, otr/, pgp/
- New test_cmd_ac.c for autocompleter unit tests
- Updated stubs for new UI suspend/resume functions
License headers
- Migrate to SPDX-3.0 identifiers (GPL-3.0-or-later WITH OpenSSL-exception)
────────────────────────────────────────────────────
cproof-specific preservations:
- XEP-0308 LMC: replace_id ?: id logic in message/stanza/omemo
- Force encryption: cmd_force_encryption, test_forced_encryption
- CWE-134: format string protection (cons_show("%s", ...))
- y_start_pos-based paging in window.c
- db_history_result_t return type, _truncate_datetime_suffix()
Merge-time fixes:
- common.c: format-security (-Werror) — cons_show(errmsg) → cons_show("%s", errmsg)
- database.c: null-deref guard — !msg->timestamp → msg && !msg->timestamp
- console.c: implicit size_t → int cast — (int)(maxlen + 1)
- tlscerts.c: %d for size_t — %zu
Build system:
- Kept autotools (Makefile.am, configure.ac); upstream uses Meson
- Restored deleted files: bootstrap.sh, autogen.sh, ax_valgrind_check.m4, configure-debug
- Updated Makefile.am test paths for subdirectory structure
- Added test_cmd_ac, test_forced_encryption to test sources
Functional tests:
- Use cproof version; upstream requires stbbr_for_xmlns from updated stabber
- Not yet available in devs/stabber fork
Closes #64
Merge author: jabber.developer2
Commits authors:
Michael Vetter <jubalh@iodoru.org>
& Steffen Jaeckel <s@jaeckel.eu>
1104 lines
41 KiB
C
1104 lines
41 KiB
C
/*
|
|
* database_sqlite.c
|
|
* vim: expandtab:ts=4:sts=4:sw=4
|
|
*
|
|
* Copyright (C) 2020 - 2025 Michael Vetter <jubalh@iodoru.org>
|
|
*
|
|
* This file is part of Profanity.
|
|
*
|
|
* Profanity 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 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Profanity 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 Profanity. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* In addition, as a special exception, the copyright holders give permission to
|
|
* link the code of portions of this program with the OpenSSL library under
|
|
* certain conditions as described in each individual source file, and
|
|
* distribute linked combinations including the two.
|
|
*
|
|
* You must obey the GNU General Public License in all respects for all of the
|
|
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
|
* may extend this exception to your version of the file(s), but you are not
|
|
* obligated to do so. If you do not wish to do so, delete this exception
|
|
* statement from your version. If you delete this exception statement from all
|
|
* source files in the program, then also delete it here.
|
|
*
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <sys/stat.h>
|
|
#include <sys/statvfs.h>
|
|
#include <sqlite3.h>
|
|
#include <glib.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
|
|
#include "log.h"
|
|
#include "common.h"
|
|
#include "config/files.h"
|
|
#include "database.h"
|
|
#include "config/preferences.h"
|
|
#include "ui/ui.h"
|
|
#include "xmpp/xmpp.h"
|
|
#include "xmpp/message.h"
|
|
|
|
static sqlite3* g_chatlog_database;
|
|
|
|
static void _add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, const Jid* const to_jid);
|
|
static char* _get_db_filename(ProfAccount* account);
|
|
static prof_msg_type_t _get_message_type_type(const char* const type);
|
|
static prof_enc_t _get_message_enc_type(const char* const encstr);
|
|
static int _get_db_version(void);
|
|
static gboolean _migrate_to_v2(void);
|
|
static gboolean _migrate_to_v3(void);
|
|
static gboolean _check_available_space_for_db_migration(char* path_to_db);
|
|
|
|
static const int latest_version = 3;
|
|
|
|
gboolean
|
|
db_sqlite_is_open(void)
|
|
{
|
|
return g_chatlog_database != NULL;
|
|
}
|
|
|
|
// Helper: close DB handle (if any), warn on busy, and shutdown SQLite
|
|
static void
|
|
_db_teardown(const char* ctx)
|
|
{
|
|
if (g_chatlog_database) {
|
|
int rc = sqlite3_close_v2(g_chatlog_database);
|
|
if (rc != SQLITE_OK) {
|
|
log_warning("sqlite3_close_v2 in %s returned %d; database may still have active statements.",
|
|
ctx ? ctx : "db_teardown", rc);
|
|
}
|
|
g_chatlog_database = NULL;
|
|
}
|
|
sqlite3_shutdown();
|
|
}
|
|
|
|
// Helper: prepare a statement and log a contextual error on failure
|
|
static gboolean
|
|
_db_prepare_ctx(const char* query, sqlite3_stmt** stmt, const char* ctx)
|
|
{
|
|
int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
log_error("SQLite error in %s: (error code: %d) %s",
|
|
ctx ? ctx : "sqlite3_prepare_v2",
|
|
rc,
|
|
sqlite3_errmsg(g_chatlog_database));
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static char*
|
|
_db_strdup(const char* str)
|
|
{
|
|
return str ? strdup(str) : NULL;
|
|
}
|
|
|
|
#define auto_sqlite __attribute__((__cleanup__(auto_free_sqlite)))
|
|
|
|
static void
|
|
auto_free_sqlite(gchar** str)
|
|
{
|
|
if (str == NULL)
|
|
return;
|
|
sqlite3_free(*str);
|
|
}
|
|
|
|
static char*
|
|
_get_db_filename(ProfAccount* account)
|
|
{
|
|
return files_file_in_account_data_path(DIR_DATABASE, account->jid, "chatlog.db");
|
|
}
|
|
|
|
static gboolean
|
|
_sqlite_init(ProfAccount* account)
|
|
{
|
|
int ret = sqlite3_initialize();
|
|
if (ret != SQLITE_OK) {
|
|
log_error("Error initializing SQLite database: %d", ret);
|
|
return FALSE;
|
|
}
|
|
|
|
auto_char char* filename = _get_db_filename(account);
|
|
if (!filename) {
|
|
log_error("Error initializing SQLite database: could not derive chatlog.db path for account '%s'.",
|
|
account && account->jid ? account->jid : "(unknown)");
|
|
sqlite3_shutdown();
|
|
return FALSE;
|
|
}
|
|
|
|
ret = sqlite3_open(filename, &g_chatlog_database);
|
|
if (ret != SQLITE_OK) {
|
|
const char* err_msg = g_chatlog_database ? sqlite3_errmsg(g_chatlog_database) : "(no handle)";
|
|
log_error("Error opening SQLite database: %s", err_msg);
|
|
_db_teardown("_sqlite_init(open)");
|
|
return FALSE;
|
|
}
|
|
|
|
char* err_msg = NULL;
|
|
|
|
int db_version = _get_db_version();
|
|
if (db_version == latest_version) {
|
|
return TRUE;
|
|
}
|
|
|
|
// ChatLogs schema overview:
|
|
// id AUTOINCREMENT primary key. Used internally as the
|
|
// anchor for LMC (Last Message Correction) chains.
|
|
// from_jid/to_jid Bare JIDs (NOT NULL). Used for window resolution.
|
|
// from_resource Resource of the sender (NULLable for legacy rows).
|
|
// to_resource Resource of the recipient (NULLable).
|
|
// message Plaintext after decryption / format normalisation.
|
|
// timestamp ISO-8601 (UTC). Indexed for range queries.
|
|
// type "chat" | "muc" | "mucpm" (mirrors prof_msg_type_t).
|
|
// stanza_id XEP-0359 unique-and-stable id, used for dedup +
|
|
// LMC sender lookup.
|
|
// archive_id MAM (XEP-0313) archive id; dedupes rebroadcast.
|
|
// encryption "none" | "omemo" | "otr" | "pgp" | "ox"
|
|
// (mirrors prof_enc_t).
|
|
// marked_read 0/1 — for unread-message badge bookkeeping.
|
|
// replace_id XEP-0308 message-correction stanza-id of the
|
|
// original message this row replaces.
|
|
// replaces_db_id Local FK back-link: id of the row this row
|
|
// corrects. Set on insert when replace_id resolves.
|
|
// replaced_by_db_id Inverse FK: id of the most recent correction.
|
|
// Maintained by the AFTER INSERT trigger below so
|
|
// readers can follow the chain forward in O(1).
|
|
const char* query = "CREATE TABLE IF NOT EXISTS `ChatLogs` ("
|
|
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
"`from_jid` TEXT NOT NULL, "
|
|
"`to_jid` TEXT NOT NULL, "
|
|
"`from_resource` TEXT, "
|
|
"`to_resource` TEXT, "
|
|
"`message` TEXT, "
|
|
"`timestamp` TEXT, "
|
|
"`type` TEXT, "
|
|
"`stanza_id` TEXT, "
|
|
"`archive_id` TEXT UNIQUE, "
|
|
"`encryption` TEXT, "
|
|
"`marked_read` INTEGER, "
|
|
"`replace_id` TEXT, "
|
|
"`replaces_db_id` INTEGER, "
|
|
"`replaced_by_db_id` INTEGER)";
|
|
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
|
goto out;
|
|
}
|
|
|
|
query = "CREATE TRIGGER IF NOT EXISTS update_corrected_message "
|
|
"AFTER INSERT ON ChatLogs "
|
|
"FOR EACH ROW "
|
|
"WHEN NEW.replaces_db_id IS NOT NULL "
|
|
"BEGIN "
|
|
"UPDATE ChatLogs "
|
|
"SET replaced_by_db_id = NEW.id "
|
|
"WHERE id = NEW.replaces_db_id; "
|
|
"END;";
|
|
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
|
log_error("Unable to add `update_corrected_message` trigger.");
|
|
goto out;
|
|
}
|
|
|
|
query = "CREATE INDEX IF NOT EXISTS ChatLogs_timestamp_IDX ON `ChatLogs` (`timestamp`)";
|
|
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
|
log_error("Unable to create index for timestamp.");
|
|
goto out;
|
|
}
|
|
query = "CREATE INDEX IF NOT EXISTS ChatLogs_to_from_jid_IDX ON `ChatLogs` (`to_jid`, `from_jid`)";
|
|
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
|
log_error("Unable to create index for to_jid.");
|
|
goto out;
|
|
}
|
|
|
|
query = "CREATE TABLE IF NOT EXISTS `DbVersion` (`dv_id` INTEGER PRIMARY KEY, `version` INTEGER UNIQUE)";
|
|
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
|
goto out;
|
|
}
|
|
|
|
if (db_version == -1) {
|
|
auto_sqlite char* init_version_query = sqlite3_mprintf(
|
|
"INSERT INTO `DbVersion` (`version`) VALUES (%d) ON CONFLICT(`version`) DO NOTHING",
|
|
latest_version);
|
|
if (!init_version_query) {
|
|
log_error("OOM allocating initial DbVersion insert query");
|
|
goto out;
|
|
}
|
|
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, init_version_query, NULL, 0, &err_msg)) {
|
|
goto out;
|
|
}
|
|
db_version = _get_db_version();
|
|
}
|
|
|
|
if (db_version == -1) {
|
|
cons_show_error("DB Initialization Error: Unable to check DB version.");
|
|
goto out;
|
|
}
|
|
|
|
if (db_version < latest_version) {
|
|
cons_show("Migrating database schema. This operation may take a while...");
|
|
if (db_version < 2 && (!_check_available_space_for_db_migration(filename) || !_migrate_to_v2())) {
|
|
cons_show_error("Database Initialization Error: Unable to migrate database to version 2. Please, check error logs for details.");
|
|
goto out;
|
|
}
|
|
if (db_version < 3 && (!_check_available_space_for_db_migration(filename) || !_migrate_to_v3())) {
|
|
cons_show_error("Database Initialization Error: Unable to migrate database to version 3. Please, check error logs for details.");
|
|
goto out;
|
|
}
|
|
cons_show("Database schema migration was successful.");
|
|
}
|
|
|
|
log_debug("Initialized SQLite database: %s", filename);
|
|
return TRUE;
|
|
|
|
out:
|
|
if (err_msg) {
|
|
log_error("SQLite error in _sqlite_init(): %s", err_msg);
|
|
sqlite3_free(err_msg);
|
|
} else {
|
|
log_error("Unknown SQLite error in _sqlite_init().");
|
|
}
|
|
_db_teardown("_sqlite_init(out)");
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
_sqlite_close(void)
|
|
{
|
|
log_debug("_sqlite_close() called");
|
|
_db_teardown("_sqlite_close");
|
|
}
|
|
|
|
static void
|
|
_sqlite_add_incoming(ProfMessage* message)
|
|
{
|
|
if (message->to_jid) {
|
|
_add_to_db(message, NULL, message->from_jid, message->to_jid);
|
|
} else {
|
|
_add_to_db(message, NULL, message->from_jid, connection_get_jid());
|
|
}
|
|
}
|
|
|
|
static void
|
|
_log_database_add_outgoing(const char* type, const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
|
|
{
|
|
ProfMessage* msg = message_init();
|
|
|
|
msg->id = _db_strdup(id);
|
|
msg->from_jid = jid_create(barejid);
|
|
msg->plain = _db_strdup(message);
|
|
msg->replace_id = _db_strdup(replace_id);
|
|
msg->timestamp = g_date_time_new_now_local();
|
|
msg->enc = enc;
|
|
|
|
_add_to_db(msg, type, connection_get_jid(), msg->from_jid);
|
|
|
|
message_free(msg);
|
|
}
|
|
|
|
static void
|
|
_sqlite_add_outgoing_chat(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
|
|
{
|
|
_log_database_add_outgoing("chat", id, barejid, message, replace_id, enc);
|
|
}
|
|
|
|
static void
|
|
_sqlite_add_outgoing_muc(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
|
|
{
|
|
_log_database_add_outgoing("muc", id, barejid, message, replace_id, enc);
|
|
}
|
|
|
|
static void
|
|
_sqlite_add_outgoing_muc_pm(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc)
|
|
{
|
|
_log_database_add_outgoing("mucpm", id, barejid, message, replace_id, enc);
|
|
}
|
|
|
|
static ProfMessage*
|
|
_sqlite_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
|
|
{
|
|
sqlite3_stmt* stmt = NULL;
|
|
const Jid* myjid = connection_get_jid();
|
|
ProfMessage* msg = message_init();
|
|
if (!myjid || !myjid->str) {
|
|
if (is_last) {
|
|
msg->timestamp = g_date_time_new_now_utc();
|
|
}
|
|
return msg;
|
|
}
|
|
|
|
const char* order = is_last ? "DESC" : "ASC";
|
|
auto_sqlite char* query = sqlite3_mprintf("SELECT `archive_id`, `timestamp` FROM `ChatLogs` WHERE "
|
|
"(`from_jid` = %Q AND `to_jid` = %Q) OR "
|
|
"(`from_jid` = %Q AND `to_jid` = %Q) "
|
|
"ORDER BY `timestamp` %s LIMIT 1;",
|
|
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid, order);
|
|
|
|
if (!query) {
|
|
log_error("Could not allocate memory for SQL query in _sqlite_get_limits_info()");
|
|
if (is_last) {
|
|
msg->timestamp = g_date_time_new_now_utc();
|
|
}
|
|
return msg;
|
|
}
|
|
|
|
if (!_db_prepare_ctx(query, &stmt, "_sqlite_get_limits_info()")) {
|
|
if (is_last) {
|
|
msg->timestamp = g_date_time_new_now_utc();
|
|
}
|
|
return msg;
|
|
}
|
|
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
char* archive_id = (char*)sqlite3_column_text(stmt, 0);
|
|
char* date = (char*)sqlite3_column_text(stmt, 1);
|
|
|
|
msg->stanzaid = _db_strdup(archive_id);
|
|
msg->timestamp = g_date_time_new_from_iso8601(date, NULL);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (!msg->timestamp && is_last) {
|
|
msg->timestamp = g_date_time_new_now_utc();
|
|
}
|
|
|
|
return msg;
|
|
}
|
|
|
|
static db_history_result_t
|
|
_sqlite_get_previous_chat(const gchar* const contact_barejid, const gchar* start_time, const gchar* end_time, gboolean from_start, gboolean flip, GSList** result)
|
|
{
|
|
if (!g_chatlog_database) {
|
|
log_warning("_sqlite_get_previous_chat() called but db is not initialized");
|
|
return DB_RESPONSE_ERROR;
|
|
}
|
|
sqlite3_stmt* stmt = NULL;
|
|
const Jid* myjid = connection_get_jid();
|
|
if (!myjid->str) {
|
|
log_warning("_sqlite_get_previous_chat() called but no connection detected.");
|
|
return DB_RESPONSE_ERROR;
|
|
}
|
|
|
|
const gchar* sort1 = from_start ? "ASC" : "DESC";
|
|
const gchar* sort2 = !flip ? "ASC" : "DESC";
|
|
GDateTime* now = g_date_time_new_now_local();
|
|
auto_gchar gchar* end_date_fmt = end_time ? g_strdup(end_time) : g_date_time_format_iso8601(now);
|
|
auto_sqlite gchar* query = sqlite3_mprintf("SELECT * FROM ("
|
|
"SELECT COALESCE(B.`message`, A.`message`) AS message, "
|
|
"A.`timestamp`, A.`from_jid`, A.`from_resource`, A.`to_jid`, A.`to_resource`, A.`type`, A.`encryption`, A.`stanza_id` FROM `ChatLogs` AS A "
|
|
"LEFT JOIN `ChatLogs` AS B ON (A.`replaced_by_db_id` = B.`id` AND A.`from_jid` = B.`from_jid`) "
|
|
"WHERE (A.`replaces_db_id` IS NULL) "
|
|
"AND ((A.`from_jid` = %Q AND A.`to_jid` = %Q) OR (A.`from_jid` = %Q AND A.`to_jid` = %Q)) "
|
|
"AND A.`timestamp` < %Q "
|
|
"AND (%Q IS NULL OR A.`timestamp` > %Q) "
|
|
"ORDER BY A.`timestamp` %s LIMIT %d) "
|
|
"ORDER BY `timestamp` %s;",
|
|
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid, end_date_fmt, start_time, start_time, sort1, MESSAGES_TO_RETRIEVE, sort2);
|
|
|
|
g_date_time_unref(now);
|
|
|
|
if (!query) {
|
|
log_error("Could not allocate memory.");
|
|
return DB_RESPONSE_ERROR;
|
|
}
|
|
|
|
if (!_db_prepare_ctx(query, &stmt, "_sqlite_get_previous_chat()")) {
|
|
return DB_RESPONSE_ERROR;
|
|
}
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
char* message = (char*)sqlite3_column_text(stmt, 0);
|
|
char* date = (char*)sqlite3_column_text(stmt, 1);
|
|
char* from_jid = (char*)sqlite3_column_text(stmt, 2);
|
|
char* from_resource = (char*)sqlite3_column_text(stmt, 3);
|
|
char* to_jid = (char*)sqlite3_column_text(stmt, 4);
|
|
char* to_resource = (char*)sqlite3_column_text(stmt, 5);
|
|
char* type = (char*)sqlite3_column_text(stmt, 6);
|
|
char* encryption = (char*)sqlite3_column_text(stmt, 7);
|
|
char* id = (char*)sqlite3_column_text(stmt, 8);
|
|
|
|
ProfMessage* msg = message_init();
|
|
msg->id = id ? strdup(id) : NULL;
|
|
msg->from_jid = jid_create_from_bare_and_resource(from_jid, from_resource);
|
|
msg->to_jid = jid_create_from_bare_and_resource(to_jid, to_resource);
|
|
msg->plain = strdup(message ?: "");
|
|
msg->timestamp = g_date_time_new_from_iso8601(date, NULL);
|
|
msg->type = _get_message_type_type(type);
|
|
msg->enc = _get_message_enc_type(encryption);
|
|
|
|
*result = g_slist_append(*result, msg);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
return g_slist_length(*result) != 0 ? DB_RESPONSE_SUCCESS : DB_RESPONSE_EMPTY;
|
|
}
|
|
|
|
static GSList*
|
|
_sqlite_verify_integrity(const gchar* const contact_barejid)
|
|
{
|
|
GSList* issues = NULL;
|
|
|
|
if (!g_chatlog_database) {
|
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
|
issue->level = INTEGRITY_ERROR;
|
|
issue->file = g_strdup("chatlog.db");
|
|
issue->line = 0;
|
|
issue->message = g_strdup("Database not initialized");
|
|
issues = g_slist_append(issues, issue);
|
|
return issues;
|
|
}
|
|
|
|
// PRAGMA integrity_check
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (_db_prepare_ctx("PRAGMA integrity_check", &stmt, "_sqlite_verify_integrity()")) {
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char* result = (const char*)sqlite3_column_text(stmt, 0);
|
|
if (g_strcmp0(result, "ok") != 0) {
|
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
|
issue->level = INTEGRITY_ERROR;
|
|
issue->file = g_strdup("chatlog.db");
|
|
issue->line = 0;
|
|
issue->message = g_strdup_printf("SQLite integrity check: %s", result);
|
|
issues = g_slist_append(issues, issue);
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
|
|
// Check timestamp ordering, restricted to consecutive rows belonging
|
|
// to the same conversation pair so adjacency does not span unrelated
|
|
// contacts. Adjacent rows by id from different conversations are
|
|
// legitimately out-of-order and should not be reported.
|
|
const Jid* myjid = connection_get_jid();
|
|
if (myjid && myjid->barejid) {
|
|
auto_sqlite char* query = NULL;
|
|
if (contact_barejid) {
|
|
query = sqlite3_mprintf(
|
|
"SELECT A.`id`, A.`timestamp`, A.`from_jid`, A.`to_jid`, B.`id`, B.`timestamp` "
|
|
"FROM `ChatLogs` A "
|
|
"JOIN `ChatLogs` B ON B.`from_jid` = A.`from_jid` AND B.`to_jid` = A.`to_jid` "
|
|
" AND B.`id` > A.`id` "
|
|
"WHERE A.`timestamp` > B.`timestamp` "
|
|
"AND ((A.`from_jid` = %Q AND A.`to_jid` = %Q) OR (A.`from_jid` = %Q AND A.`to_jid` = %Q)) "
|
|
"AND NOT EXISTS (SELECT 1 FROM `ChatLogs` C "
|
|
" WHERE C.`from_jid` = A.`from_jid` AND C.`to_jid` = A.`to_jid` "
|
|
" AND C.`id` > A.`id` AND C.`id` < B.`id`) "
|
|
"LIMIT 50",
|
|
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid);
|
|
} else {
|
|
query = sqlite3_mprintf(
|
|
"SELECT A.`id`, A.`timestamp`, A.`from_jid`, A.`to_jid`, B.`id`, B.`timestamp` "
|
|
"FROM `ChatLogs` A "
|
|
"JOIN `ChatLogs` B ON B.`from_jid` = A.`from_jid` AND B.`to_jid` = A.`to_jid` "
|
|
" AND B.`id` > A.`id` "
|
|
"WHERE A.`timestamp` > B.`timestamp` "
|
|
"AND NOT EXISTS (SELECT 1 FROM `ChatLogs` C "
|
|
" WHERE C.`from_jid` = A.`from_jid` AND C.`to_jid` = A.`to_jid` "
|
|
" AND C.`id` > A.`id` AND C.`id` < B.`id`) "
|
|
"LIMIT 50");
|
|
}
|
|
|
|
if (query && _db_prepare_ctx(query, &stmt, "_sqlite_verify_integrity(timestamp_order)")) {
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
int id_a = sqlite3_column_int(stmt, 0);
|
|
const char* ts_a = (const char*)sqlite3_column_text(stmt, 1);
|
|
const char* from_a = (const char*)sqlite3_column_text(stmt, 2);
|
|
const char* to_a = (const char*)sqlite3_column_text(stmt, 3);
|
|
int id_b = sqlite3_column_int(stmt, 4);
|
|
const char* ts_b = (const char*)sqlite3_column_text(stmt, 5);
|
|
|
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
|
issue->level = INTEGRITY_WARNING;
|
|
issue->file = g_strdup_printf("%s↔%s/chatlog.db",
|
|
from_a ? from_a : "?", to_a ? to_a : "?");
|
|
issue->line = id_a;
|
|
issue->message = g_strdup_printf("Timestamp out of order: row %d (%s) > row %d (%s)",
|
|
id_a, ts_a ? ts_a : "NULL",
|
|
id_b, ts_b ? ts_b : "NULL");
|
|
issues = g_slist_append(issues, issue);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
|
|
// Check broken LMC references
|
|
auto_sqlite char* lmc_query = sqlite3_mprintf(
|
|
"SELECT A.`id`, A.`replaces_db_id`, A.`from_jid`, A.`to_jid` FROM `ChatLogs` A "
|
|
"WHERE A.`replaces_db_id` IS NOT NULL "
|
|
"AND NOT EXISTS (SELECT 1 FROM `ChatLogs` B WHERE B.`id` = A.`replaces_db_id`) "
|
|
"LIMIT 50");
|
|
|
|
if (lmc_query && _db_prepare_ctx(lmc_query, &stmt, "_sqlite_verify_integrity(lmc_check)")) {
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
int id = sqlite3_column_int(stmt, 0);
|
|
int replaces_id = sqlite3_column_int(stmt, 1);
|
|
const char* from = (const char*)sqlite3_column_text(stmt, 2);
|
|
const char* to = (const char*)sqlite3_column_text(stmt, 3);
|
|
|
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
|
issue->level = INTEGRITY_ERROR;
|
|
issue->file = g_strdup_printf("%s↔%s/chatlog.db",
|
|
from ? from : "?", to ? to : "?");
|
|
issue->line = id;
|
|
issue->message = g_strdup_printf("Broken LMC reference: row %d references non-existent row %d",
|
|
id, replaces_id);
|
|
issues = g_slist_append(issues, issue);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
// --- Type conversion helpers ---
|
|
|
|
static const char*
|
|
_get_message_type_str(prof_msg_type_t type)
|
|
{
|
|
switch (type) {
|
|
case PROF_MSG_TYPE_CHAT:
|
|
return "chat";
|
|
case PROF_MSG_TYPE_MUC:
|
|
return "muc";
|
|
case PROF_MSG_TYPE_MUCPM:
|
|
return "mucpm";
|
|
case PROF_MSG_TYPE_UNINITIALIZED:
|
|
return NULL;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static prof_msg_type_t
|
|
_get_message_type_type(const char* const type)
|
|
{
|
|
if (g_strcmp0(type, "chat") == 0) {
|
|
return PROF_MSG_TYPE_CHAT;
|
|
} else if (g_strcmp0(type, "muc") == 0) {
|
|
return PROF_MSG_TYPE_MUC;
|
|
} else if (g_strcmp0(type, "mucpm") == 0) {
|
|
return PROF_MSG_TYPE_MUCPM;
|
|
} else {
|
|
return PROF_MSG_TYPE_UNINITIALIZED;
|
|
}
|
|
}
|
|
|
|
static const char*
|
|
_get_message_enc_str(prof_enc_t enc)
|
|
{
|
|
switch (enc) {
|
|
case PROF_MSG_ENC_OX:
|
|
return "ox";
|
|
case PROF_MSG_ENC_PGP:
|
|
return "pgp";
|
|
case PROF_MSG_ENC_OTR:
|
|
return "otr";
|
|
case PROF_MSG_ENC_OMEMO:
|
|
return "omemo";
|
|
case PROF_MSG_ENC_NONE:
|
|
return "none";
|
|
}
|
|
|
|
return "none";
|
|
}
|
|
|
|
static prof_enc_t
|
|
_get_message_enc_type(const char* const encstr)
|
|
{
|
|
if (g_strcmp0(encstr, "ox") == 0) {
|
|
return PROF_MSG_ENC_OX;
|
|
} else if (g_strcmp0(encstr, "pgp") == 0) {
|
|
return PROF_MSG_ENC_PGP;
|
|
} else if (g_strcmp0(encstr, "otr") == 0) {
|
|
return PROF_MSG_ENC_OTR;
|
|
} else if (g_strcmp0(encstr, "omemo") == 0) {
|
|
return PROF_MSG_ENC_OMEMO;
|
|
}
|
|
|
|
return PROF_MSG_ENC_NONE;
|
|
}
|
|
|
|
// --- Core write logic ---
|
|
|
|
static void
|
|
_add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, const Jid* const to_jid)
|
|
{
|
|
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
|
|
sqlite_int64 original_message_id = -1;
|
|
|
|
if (g_strcmp0(pref_dblog, "off") == 0) {
|
|
return;
|
|
} else if (g_strcmp0(pref_dblog, "redact") == 0) {
|
|
if (message->plain) {
|
|
free(message->plain);
|
|
}
|
|
message->plain = strdup("[REDACTED]");
|
|
}
|
|
|
|
if (!g_chatlog_database) {
|
|
log_debug("_add_to_db() called but db is not initialized");
|
|
return;
|
|
}
|
|
|
|
char* err_msg;
|
|
auto_gchar gchar* date_fmt = NULL;
|
|
|
|
if (message->timestamp) {
|
|
date_fmt = g_date_time_format_iso8601(message->timestamp);
|
|
} else {
|
|
GDateTime* dt = g_date_time_new_now_local();
|
|
date_fmt = g_date_time_format_iso8601(dt);
|
|
g_date_time_unref(dt);
|
|
}
|
|
|
|
const char* enc = _get_message_enc_str(message->enc);
|
|
|
|
if (!type) {
|
|
type = (char*)_get_message_type_str(message->type);
|
|
}
|
|
|
|
// Apply LMC and check its validity (XEP-0308)
|
|
if (message->replace_id) {
|
|
auto_sqlite char* replace_check_query = sqlite3_mprintf("SELECT `id`, `from_jid`, `replaces_db_id` FROM `ChatLogs` WHERE `stanza_id` = %Q ORDER BY `timestamp` DESC LIMIT 1",
|
|
message->replace_id);
|
|
|
|
if (!replace_check_query) {
|
|
log_error("Could not allocate memory for SQL replace query in _add_to_db()");
|
|
return;
|
|
}
|
|
|
|
sqlite3_stmt* lmc_stmt = NULL;
|
|
if (!_db_prepare_ctx(replace_check_query, &lmc_stmt, "_add_to_db(replace_check)")) {
|
|
return;
|
|
}
|
|
|
|
if (sqlite3_step(lmc_stmt) == SQLITE_ROW) {
|
|
original_message_id = sqlite3_column_int64(lmc_stmt, 0);
|
|
const char* from_jid_orig = (const char*)sqlite3_column_text(lmc_stmt, 1);
|
|
|
|
sqlite_int64 tmp = sqlite3_column_int64(lmc_stmt, 2);
|
|
original_message_id = tmp ? tmp : original_message_id;
|
|
|
|
if (g_strcmp0(from_jid_orig, from_jid->barejid) != 0) {
|
|
log_error("Mismatch in sender JIDs when trying to do LMC. Corrected message sender: %s. Original message sender: %s. Replace-ID: %s. Message: %s", from_jid->barejid, from_jid_orig, message->replace_id, message->plain);
|
|
cons_show_error("%s sent a message correction with mismatched sender. See log for details.", from_jid->barejid);
|
|
sqlite3_finalize(lmc_stmt);
|
|
return;
|
|
}
|
|
} else {
|
|
log_warning("Got LMC message that does not have original message counterpart in the database from %s", message->from_jid->fulljid);
|
|
}
|
|
sqlite3_finalize(lmc_stmt);
|
|
}
|
|
|
|
if (message->stanzaid && !message->is_mam) {
|
|
auto_sqlite char* duplicate_check_query = sqlite3_mprintf("SELECT 1 FROM `ChatLogs` WHERE (`archive_id` = %Q)",
|
|
message->stanzaid);
|
|
|
|
if (!duplicate_check_query) {
|
|
log_error("Could not allocate memory for SQL duplicate query in _add_to_db()");
|
|
return;
|
|
}
|
|
|
|
sqlite3_stmt* stmt;
|
|
if (_db_prepare_ctx(duplicate_check_query, &stmt, "_add_to_db(duplicate_check)")) {
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
log_error("Duplicate stanza-id found for the message. stanza_id: %s; archive_id: %s; sender: %s; content: %s", message->id, message->stanzaid, from_jid->barejid, message->plain);
|
|
cons_show_error("Got a message with duplicate (server-generated) stanza-id from %s.", from_jid->fulljid);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
}
|
|
|
|
auto_sqlite char* orig_message_id = original_message_id == -1 ? NULL : sqlite3_mprintf("%d", original_message_id);
|
|
|
|
auto_sqlite char* query = sqlite3_mprintf("INSERT INTO `ChatLogs` "
|
|
"(`from_jid`, `from_resource`, `to_jid`, `to_resource`, "
|
|
"`message`, `timestamp`, `stanza_id`, `archive_id`, "
|
|
"`replaces_db_id`, `replace_id`, `type`, `encryption`) "
|
|
"VALUES (%Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q)",
|
|
from_jid->barejid,
|
|
from_jid->resourcepart,
|
|
to_jid->barejid,
|
|
to_jid->resourcepart,
|
|
message->plain,
|
|
date_fmt,
|
|
message->id,
|
|
message->stanzaid,
|
|
orig_message_id,
|
|
message->replace_id,
|
|
type,
|
|
enc);
|
|
if (!query) {
|
|
log_error("Could not allocate memory for SQL insert query in _add_to_db()");
|
|
return;
|
|
}
|
|
|
|
log_debug("Writing to DB. Query: %s", query);
|
|
|
|
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
|
if (err_msg) {
|
|
log_error("SQLite error in _add_to_db(): %s", err_msg);
|
|
sqlite3_free(err_msg);
|
|
} else {
|
|
log_error("Unknown SQLite error in _add_to_db().");
|
|
}
|
|
} else {
|
|
int inserted_rows_count = sqlite3_changes(g_chatlog_database);
|
|
if (inserted_rows_count < 1) {
|
|
log_error("SQLite did not insert message (rows: %d, id: %s, content: %s)", inserted_rows_count, message->id, message->plain);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- DB version and migration ---
|
|
|
|
static int
|
|
_get_db_version(void)
|
|
{
|
|
int current_version = -1;
|
|
const char* query = "SELECT `version` FROM `DbVersion` LIMIT 1";
|
|
sqlite3_stmt* statement;
|
|
if (_db_prepare_ctx(query, &statement, "_get_db_version()")) {
|
|
if (sqlite3_step(statement) == SQLITE_ROW) {
|
|
current_version = sqlite3_column_int(statement, 0);
|
|
}
|
|
sqlite3_finalize(statement);
|
|
}
|
|
return current_version;
|
|
}
|
|
|
|
static gboolean
|
|
_migrate_to_v2(void)
|
|
{
|
|
char* err_msg = NULL;
|
|
|
|
const char* sql_statements[] = {
|
|
"BEGIN TRANSACTION",
|
|
"ALTER TABLE `ChatLogs` ADD COLUMN `replaces_db_id` INTEGER;",
|
|
"ALTER TABLE `ChatLogs` ADD COLUMN `replaced_by_db_id` INTEGER;",
|
|
"UPDATE `ChatLogs` AS A "
|
|
"SET `replaces_db_id` = B.`id` "
|
|
"FROM `ChatLogs` AS B "
|
|
"WHERE A.`replace_id` IS NOT NULL AND A.`replace_id` != '' "
|
|
"AND A.`replace_id` = B.`stanza_id` "
|
|
"AND A.`from_jid` = B.`from_jid` AND A.`to_jid` = B.`to_jid`;",
|
|
"UPDATE `ChatLogs` AS A "
|
|
"SET `replaced_by_db_id` = B.`id` "
|
|
"FROM `ChatLogs` AS B "
|
|
"WHERE (A.`replace_id` IS NULL OR A.`replace_id` = '') "
|
|
"AND A.`id` = B.`replaces_db_id` "
|
|
"AND A.`from_jid` = B.`from_jid`;",
|
|
"UPDATE ChatLogs SET "
|
|
"from_resource = COALESCE(NULLIF(from_resource, ''), NULL), "
|
|
"to_resource = COALESCE(NULLIF(to_resource, ''), NULL), "
|
|
"message = COALESCE(NULLIF(message, ''), NULL), "
|
|
"timestamp = COALESCE(NULLIF(timestamp, ''), NULL), "
|
|
"stanza_id = COALESCE(NULLIF(stanza_id, ''), NULL), "
|
|
"archive_id = COALESCE(NULLIF(archive_id, ''), NULL), "
|
|
"replace_id = COALESCE(NULLIF(replace_id, ''), NULL), "
|
|
"type = COALESCE(NULLIF(type, ''), NULL), "
|
|
"encryption = COALESCE(NULLIF(encryption, ''), NULL);",
|
|
"DELETE FROM `DbVersion`;",
|
|
"INSERT INTO `DbVersion` (`version`) VALUES (2);",
|
|
"END TRANSACTION"
|
|
};
|
|
|
|
int statements_count = sizeof(sql_statements) / sizeof(sql_statements[0]);
|
|
|
|
for (int i = 0; i < statements_count; i++) {
|
|
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, sql_statements[i], NULL, 0, &err_msg)) {
|
|
log_error("SQLite error in _migrate_to_v2() on statement %d: %s", i, err_msg);
|
|
if (err_msg) {
|
|
sqlite3_free(err_msg);
|
|
err_msg = NULL;
|
|
}
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
|
|
cleanup:
|
|
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, "ROLLBACK;", NULL, 0, &err_msg)) {
|
|
log_error("[DB Migration] Unable to ROLLBACK: %s", err_msg);
|
|
if (err_msg) {
|
|
sqlite3_free(err_msg);
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
_migrate_to_v3(void)
|
|
{
|
|
char* err_msg = NULL;
|
|
|
|
const char* sql_statements[] = {
|
|
"BEGIN TRANSACTION",
|
|
"CREATE TABLE `ChatLogs_v3_migration` ("
|
|
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
"`from_jid` TEXT NOT NULL, "
|
|
"`to_jid` TEXT NOT NULL, "
|
|
"`from_resource` TEXT, "
|
|
"`to_resource` TEXT, "
|
|
"`message` TEXT, "
|
|
"`timestamp` TEXT, "
|
|
"`type` TEXT, "
|
|
"`stanza_id` TEXT, "
|
|
"`archive_id` TEXT UNIQUE, "
|
|
"`encryption` TEXT, "
|
|
"`marked_read` INTEGER, "
|
|
"`replace_id` TEXT, "
|
|
"`replaces_db_id` INTEGER, "
|
|
"`replaced_by_db_id` INTEGER)",
|
|
|
|
"INSERT INTO `ChatLogs_v3_migration` "
|
|
"SELECT * FROM `ChatLogs` "
|
|
"WHERE `archive_id` IS NULL "
|
|
"UNION ALL "
|
|
"SELECT * FROM (SELECT * FROM `ChatLogs` WHERE `archive_id` IS NOT NULL GROUP BY `archive_id`)",
|
|
|
|
"DROP TABLE `ChatLogs` ",
|
|
"ALTER TABLE `ChatLogs_v3_migration` RENAME TO `ChatLogs` ",
|
|
|
|
"CREATE INDEX ChatLogs_timestamp_IDX ON `ChatLogs` (`timestamp`)",
|
|
"CREATE INDEX ChatLogs_to_from_jid_IDX ON `ChatLogs` (`to_jid`, `from_jid`)",
|
|
"CREATE TRIGGER update_corrected_message "
|
|
"AFTER INSERT ON ChatLogs "
|
|
"FOR EACH ROW "
|
|
"WHEN NEW.replaces_db_id IS NOT NULL "
|
|
"BEGIN "
|
|
"UPDATE ChatLogs "
|
|
"SET replaced_by_db_id = NEW.id "
|
|
"WHERE id = NEW.replaces_db_id; "
|
|
"END;",
|
|
|
|
"DELETE FROM `DbVersion`;",
|
|
"INSERT INTO `DbVersion` (`version`) VALUES (3);",
|
|
"END TRANSACTION"
|
|
};
|
|
|
|
int statements_count = sizeof(sql_statements) / sizeof(sql_statements[0]);
|
|
|
|
for (int i = 0; i < statements_count; i++) {
|
|
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, sql_statements[i], NULL, 0, &err_msg)) {
|
|
log_error("SQLite error in _migrate_to_v3() on statement %d: %s", i, err_msg);
|
|
if (err_msg) {
|
|
sqlite3_free(err_msg);
|
|
err_msg = NULL;
|
|
}
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
|
|
cleanup:
|
|
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, "ROLLBACK;", NULL, 0, &err_msg)) {
|
|
log_error("[DB Migration] Unable to ROLLBACK: %s", err_msg);
|
|
if (err_msg) {
|
|
sqlite3_free(err_msg);
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
_check_available_space_for_db_migration(char* path_to_db)
|
|
{
|
|
struct stat file_stat;
|
|
struct statvfs fs_stat;
|
|
|
|
if (statvfs(path_to_db, &fs_stat) == 0 && stat(path_to_db, &file_stat) == 0) {
|
|
unsigned long long file_size = file_stat.st_size / 1024;
|
|
unsigned long long available_space_kb = fs_stat.f_frsize * fs_stat.f_bavail / 1024;
|
|
log_debug("_check_available_space_for_db_migration(): Available space on disk: %llu KB; DB size: %llu KB", available_space_kb, file_size);
|
|
|
|
return (available_space_kb >= (file_size + (file_size * 10 / 4)));
|
|
} else {
|
|
log_error("Error checking available space.");
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
// --- Backend vtable ---
|
|
|
|
static db_backend_t sqlite_backend = {
|
|
.name = "sqlite",
|
|
.init = _sqlite_init,
|
|
.close = _sqlite_close,
|
|
.add_incoming = _sqlite_add_incoming,
|
|
.add_outgoing_chat = _sqlite_add_outgoing_chat,
|
|
.add_outgoing_muc = _sqlite_add_outgoing_muc,
|
|
.add_outgoing_muc_pm = _sqlite_add_outgoing_muc_pm,
|
|
.get_previous_chat = _sqlite_get_previous_chat,
|
|
.get_limits_info = _sqlite_get_limits_info,
|
|
.verify_integrity = _sqlite_verify_integrity,
|
|
};
|
|
|
|
db_backend_t*
|
|
db_backend_sqlite(void)
|
|
{
|
|
return &sqlite_backend;
|
|
}
|
|
|
|
void
|
|
db_sqlite_begin_transaction(void)
|
|
{
|
|
if (!g_chatlog_database)
|
|
return;
|
|
char* err_msg = NULL;
|
|
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, "BEGIN TRANSACTION", NULL, 0, &err_msg)) {
|
|
if (err_msg) {
|
|
log_error("SQLite BEGIN TRANSACTION failed: %s", err_msg);
|
|
sqlite3_free(err_msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
db_sqlite_end_transaction(void)
|
|
{
|
|
if (!g_chatlog_database)
|
|
return;
|
|
char* err_msg = NULL;
|
|
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, "END TRANSACTION", NULL, 0, &err_msg)) {
|
|
if (err_msg) {
|
|
log_error("SQLite END TRANSACTION failed: %s", err_msg);
|
|
sqlite3_free(err_msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
db_sqlite_rollback_transaction(void)
|
|
{
|
|
if (!g_chatlog_database)
|
|
return;
|
|
char* err_msg = NULL;
|
|
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, "ROLLBACK", NULL, 0, &err_msg)) {
|
|
if (err_msg) {
|
|
log_error("SQLite ROLLBACK failed: %s", err_msg);
|
|
sqlite3_free(err_msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
int
|
|
db_sqlite_last_changes(void)
|
|
{
|
|
if (!g_chatlog_database)
|
|
return 0;
|
|
return sqlite3_changes(g_chatlog_database);
|
|
}
|
|
|
|
GSList*
|
|
db_sqlite_get_all_chat(const gchar* const contact_barejid)
|
|
{
|
|
GSList* result = NULL;
|
|
|
|
if (!g_chatlog_database)
|
|
return NULL;
|
|
|
|
const Jid* myjid = connection_get_jid();
|
|
if (!myjid || !myjid->barejid)
|
|
return NULL;
|
|
|
|
sqlite3_stmt* stmt = NULL;
|
|
auto_sqlite char* query = sqlite3_mprintf(
|
|
"SELECT COALESCE(B.`message`, A.`message`) AS message, "
|
|
"A.`timestamp`, A.`from_jid`, A.`from_resource`, A.`to_jid`, A.`to_resource`, "
|
|
"A.`type`, A.`encryption`, A.`stanza_id`, A.`archive_id`, A.`marked_read` "
|
|
"FROM `ChatLogs` AS A "
|
|
"LEFT JOIN `ChatLogs` AS B ON (A.`replaced_by_db_id` = B.`id` AND A.`from_jid` = B.`from_jid`) "
|
|
"WHERE (A.`replaces_db_id` IS NULL) "
|
|
"AND ((A.`from_jid` = %Q AND A.`to_jid` = %Q) OR (A.`from_jid` = %Q AND A.`to_jid` = %Q)) "
|
|
"ORDER BY A.`timestamp` ASC",
|
|
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid);
|
|
|
|
if (!query)
|
|
return NULL;
|
|
|
|
if (!_db_prepare_ctx(query, &stmt, "db_sqlite_get_all_chat()"))
|
|
return NULL;
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
char* message = (char*)sqlite3_column_text(stmt, 0);
|
|
char* date = (char*)sqlite3_column_text(stmt, 1);
|
|
char* from_jid = (char*)sqlite3_column_text(stmt, 2);
|
|
char* from_resource = (char*)sqlite3_column_text(stmt, 3);
|
|
char* to_jid = (char*)sqlite3_column_text(stmt, 4);
|
|
char* to_resource = (char*)sqlite3_column_text(stmt, 5);
|
|
char* type = (char*)sqlite3_column_text(stmt, 6);
|
|
char* encryption = (char*)sqlite3_column_text(stmt, 7);
|
|
char* id = (char*)sqlite3_column_text(stmt, 8);
|
|
char* archive_id = (char*)sqlite3_column_text(stmt, 9);
|
|
int marked_read_raw = sqlite3_column_type(stmt, 10) == SQLITE_NULL ? -1 : sqlite3_column_int(stmt, 10);
|
|
|
|
ProfMessage* msg = message_init();
|
|
msg->id = _db_strdup(id);
|
|
msg->stanzaid = _db_strdup(archive_id);
|
|
msg->from_jid = jid_create_from_bare_and_resource(from_jid, from_resource);
|
|
msg->to_jid = jid_create_from_bare_and_resource(to_jid, to_resource);
|
|
msg->plain = _db_strdup(message);
|
|
if (!msg->plain)
|
|
msg->plain = strdup("");
|
|
msg->timestamp = g_date_time_new_from_iso8601(date, NULL);
|
|
msg->type = _get_message_type_type(type);
|
|
msg->enc = _get_message_enc_type(encryption);
|
|
msg->marked_read = marked_read_raw;
|
|
|
|
result = g_slist_prepend(result, msg);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
return g_slist_reverse(result);
|
|
}
|
|
|
|
GSList*
|
|
db_sqlite_list_contacts(void)
|
|
{
|
|
GSList* contacts = NULL;
|
|
if (!g_chatlog_database)
|
|
return NULL;
|
|
|
|
const Jid* myjid = connection_get_jid();
|
|
if (!myjid || !myjid->barejid)
|
|
return NULL;
|
|
|
|
const char* q = "SELECT DISTINCT `from_jid` FROM `ChatLogs` "
|
|
"WHERE `from_jid` != ? "
|
|
"UNION "
|
|
"SELECT DISTINCT `to_jid` FROM `ChatLogs` "
|
|
"WHERE `to_jid` != ?";
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (!_db_prepare_ctx(q, &stmt, "db_sqlite_list_contacts"))
|
|
return NULL;
|
|
|
|
sqlite3_bind_text(stmt, 1, myjid->barejid, -1, SQLITE_STATIC);
|
|
sqlite3_bind_text(stmt, 2, myjid->barejid, -1, SQLITE_STATIC);
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char* jid = (const char*)sqlite3_column_text(stmt, 0);
|
|
if (jid && strlen(jid) > 0) {
|
|
contacts = g_slist_prepend(contacts, g_strdup(jid));
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
return g_slist_reverse(contacts);
|
|
} |