Merge upstream/master into merge/upstream-full
All checks were successful
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Check spelling (pull_request) Successful in 15s
CI Code / Linux (debian) (pull_request) Successful in 6m43s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m49s
CI Code / Linux (arch) (pull_request) Successful in 8m52s
CI Code / Code Coverage (pull_request) Successful in 7m19s
All checks were successful
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Check spelling (pull_request) Successful in 15s
CI Code / Linux (debian) (pull_request) Successful in 6m43s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m49s
CI Code / Linux (arch) (pull_request) Successful in 8m52s
CI Code / Code Coverage (pull_request) Successful in 7m19s
Merge profanity-im/profanity master (373 commits) into cproof fork.
Source changes (manual merge):
- src/command/: cmd_defs, cmd_funcs, cmd_ac — upstream g_new0, auto_gchar,
launch_editor callback; keep our XEP-0308 LMC, force-encryption, CWE-134
- src/config/: preferences, tlscerts, account — upstream UNIQUE dedup,
dynamic pad capacity; keep our db_history_result_t, scroll logic
- src/database.*: upstream g_new0 memory mgmt; keep our return types,
add null-check fix for msg->timestamp
- src/omemo/, src/pgp/: upstream _gpgme_key_get_email, g_new0;
keep our replace_id in omemo_on_message_send
- src/tools/: editor, http_upload, bookmark_ignore — upstream launch_editor
callback API
- src/ui/: console, window, chatwin, mucwin, inputwin, buffer —
upstream win_warn_needed/sent dedup, PAD_MIN_HEIGHT dynamic pads;
keep our y_start_pos scroll, _truncate_datetime_suffix
- src/xmpp/: message, stanza, presence, roster_list, iq, session —
upstream connection_get_available_resources; keep our LMC stanza logic
- src/common.c: fix format-security (cons_show)
Build system:
- Makefile.am: updated test paths to subdirectory structure, added
test_cmd_ac, test_forced_encryption
- Restored autotools files deleted by upstream meson migration:
bootstrap.sh, autogen.sh, m4/ax_valgrind_check.m4, configure-debug
Tests:
- Unit tests: upstream unittests.c base + our forced_encryption tests
(482 passed, 0 failed across all 4 configurations)
- Functional tests: kept our version (93 passed, 0 failed)
upstream version requires stbbr_for_xmlns not yet in our stabber fork
- Updated test stubs: stub_xmpp.c, stub_omemo.c from upstream
Compile fixes:
- src/common.c: cons_show(errmsg) -> cons_show("%s", errmsg)
- src/database.c: null-check before msg->timestamp access
- src/ui/console.c: size_t -> (int) cast for format width
- src/config/tlscerts.c: %d -> %zu for size_t
This commit is contained in:
221
src/database.c
221
src/database.c
@@ -2,35 +2,9 @@
|
||||
* database.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.
|
||||
* Copyright (C) 2020 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -61,9 +35,10 @@ 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 = 2;
|
||||
static const int latest_version = 3;
|
||||
|
||||
// Helper: close DB handle (if any), warn on busy, and shutdown SQLite
|
||||
static void
|
||||
@@ -167,22 +142,22 @@ log_database_init(ProfAccount* account)
|
||||
// replace_id is the ID from XEP-0308: Last Message Correction
|
||||
// replaces_db_id is ID (primary key) of the original message that LMC message corrects/replaces
|
||||
// replaced_by_db_id is ID (primary key) of the last correcting (LMC) message for the original message
|
||||
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, "
|
||||
"`encryption` TEXT, "
|
||||
"`marked_read` INTEGER, "
|
||||
"`replace_id` TEXT, "
|
||||
"`replaces_db_id` INTEGER, "
|
||||
"`replaced_by_db_id` INTEGER)";
|
||||
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;
|
||||
}
|
||||
@@ -218,7 +193,7 @@ log_database_init(ProfAccount* account)
|
||||
}
|
||||
|
||||
if (db_version == -1) {
|
||||
query = "INSERT OR IGNORE INTO `DbVersion` (`version`) VALUES ('2')";
|
||||
query = "INSERT INTO `DbVersion` (`version`) VALUES ('2') ON CONFLICT(`version`) DO NOTHING";
|
||||
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
||||
goto out;
|
||||
}
|
||||
@@ -237,6 +212,10 @@ log_database_init(ProfAccount* account)
|
||||
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.");
|
||||
}
|
||||
|
||||
@@ -350,11 +329,14 @@ log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_las
|
||||
|
||||
msg->stanzaid = _db_strdup(archive_id);
|
||||
msg->timestamp = g_date_time_new_from_iso8601(date, NULL);
|
||||
} else {
|
||||
message_free(msg);
|
||||
msg = NULL;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// If nothing was found and caller expects the last message, provide a sane default
|
||||
if (!msg->timestamp && is_last) {
|
||||
if (msg && !msg->timestamp && is_last) {
|
||||
msg->timestamp = g_date_time_new_now_utc();
|
||||
}
|
||||
|
||||
@@ -381,8 +363,7 @@ log_database_get_previous_chat(const gchar* const contact_barejid, const gchar*
|
||||
// Flip order when querying older pages
|
||||
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_gchar gchar* end_date_fmt = end_time ? g_strdup(end_time) : prof_date_time_format_iso8601(NULL);
|
||||
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 "
|
||||
@@ -395,8 +376,6 @@ log_database_get_previous_chat(const gchar* const contact_barejid, const gchar*
|
||||
"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;
|
||||
@@ -518,17 +497,7 @@ _add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, co
|
||||
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);
|
||||
}
|
||||
|
||||
auto_gchar gchar* date_fmt = prof_date_time_format_iso8601(message->timestamp);
|
||||
const char* enc = _get_message_enc_str(message->enc);
|
||||
|
||||
if (!type) {
|
||||
@@ -571,26 +540,7 @@ _add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, co
|
||||
}
|
||||
|
||||
// stanza-id (XEP-0359) doesn't have to be present in the message.
|
||||
// But if it's duplicated, it's a serious server-side problem, so we better track it.
|
||||
// Unless it's MAM, in that case it's expected behaviour.
|
||||
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 log_database_add()");
|
||||
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);
|
||||
}
|
||||
}
|
||||
// We use archive_id UNIQUE constraint and ON CONFLICT DO NOTHING for deduplication.
|
||||
|
||||
auto_sqlite char* orig_message_id = original_message_id == -1 ? NULL : sqlite3_mprintf("%d", original_message_id);
|
||||
|
||||
@@ -598,7 +548,9 @@ _add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, co
|
||||
"(`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)",
|
||||
"VALUES (%Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q) "
|
||||
"ON CONFLICT(`archive_id`) DO NOTHING "
|
||||
"RETURNING id",
|
||||
from_jid->barejid,
|
||||
from_jid->resourcepart,
|
||||
to_jid->barejid,
|
||||
@@ -618,19 +570,23 @@ _add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, co
|
||||
|
||||
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);
|
||||
}
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
log_error("SQLite error in _add_to_db() (prepare): %s", sqlite3_errmsg(g_chatlog_database));
|
||||
return;
|
||||
}
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_ROW) {
|
||||
log_debug("Successfully inserted message into database.");
|
||||
} else if (rc == SQLITE_DONE) {
|
||||
log_debug("Message already exists in database (archive_id: %s), skipping.", message->stanzaid);
|
||||
} else {
|
||||
log_error("SQLite error in _add_to_db() (step): %s", sqlite3_errmsg(g_chatlog_database));
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
static int
|
||||
@@ -735,3 +691,78 @@ _check_available_space_for_db_migration(char* path_to_db)
|
||||
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;",
|
||||
|
||||
"UPDATE `DbVersion` SET `version` = 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user