Compare commits

..

4 Commits

Author SHA1 Message Date
53cdf488b6 fix(ai): resolve UAF and require provider argument instead of defaults
All checks were successful
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Check coding style (pull_request) Successful in 50s
CI Code / Linux (debian) (pull_request) Successful in 4m57s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m12s
CI Code / Code Coverage (pull_request) Successful in 6m48s
CI Code / Linux (arch) (pull_request) Successful in 11m9s
CI Code / Check spelling (push) Successful in 16s
CI Code / Check coding style (push) Successful in 33s
CI Code / Code Coverage (push) Successful in 2m41s
CI Code / Linux (debian) (push) Successful in 4m40s
CI Code / Linux (ubuntu) (push) Successful in 4m53s
CI Code / Linux (arch) (push) Successful in 10m41s
This enforces explicit provider specification.
Automatic fallback to preferences or "openai" is removed.
Users must now pass the provider name explicitly.

default provider functionality was improperly removed and implemented (some parts still remain intact), leading to UAF.
2026-05-19 16:42:22 +00:00
4776c1f1ec fix(ai): properly decode \uXXXX JSON escape sequences as UTF-8
Add helper functions to parse hex digits and encode UTF-8 characters. Update buffer allocation to account for UTF-8 expansion and implement full surrogate pair support for characters outside the BMP. Previously, \uXXXX sequences were passed through verbatim; they are now correctly decoded into proper UTF-8 strings.
2026-05-19 16:40:21 +00:00
a3a45ad477 fix(ui): preserve messages in non-chat windows while scrolled
All checks were successful
CI Code / Check spelling (push) Successful in 17s
CI Code / Check coding style (push) Successful in 31s
CI Code / Code Coverage (push) Successful in 2m43s
CI Code / Linux (debian) (push) Successful in 4m33s
CI Code / Linux (ubuntu) (push) Successful in 4m48s
CI Code / Linux (arch) (push) Successful in 5m36s
_win_printf dropped buffer append and render for any window in paged
state, but only WIN_CHAT can recover lost messages via chatwin_db_history()
on scroll-down. WIN_MUC, WIN_PRIVATE and WIN_AI have no such fallback, so
incoming and outgoing messages were silently lost when the user was viewing
history.

Introduce log_database_can_recover_messages() to check whether the DB
backend can replay messages (returns FALSE when PREF_DBLOG is "off",
"redact", or no backend is active). Gate the WIN_CHAT early return in
_win_printf() on this check: when recovery is impossible, fall through
and append to the buffer so messages remain visible on scroll-down.

Add a buffer-bottom reset in win_page_down() for non-chat windows.
WIN_SCROLL_REACHED_BOTTOM is only set on the is_chat DB branch, so
non-chat windows never clear paged on their own; reset paged and
unread_msg when the last line of the buffer reaches the screen.

Add missing scroll rendering for AI windows in the title bar draw
function, ensuring AI windows display their scrolled state consistently
with other window types.

Remove the manual paged/unread_msg reset before printing the user
message in cl_ev_send_ai_msg() -- it was a local workaround for the
same drop and is no longer needed.
2026-05-16 15:47:49 +00:00
2952466abd feat(history): consolidate logging controls and add dbbackend statusbar indicator
All checks were successful
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 33s
CI Code / Code Coverage (push) Successful in 2m42s
CI Code / Linux (debian) (push) Successful in 4m40s
CI Code / Linux (ubuntu) (push) Successful in 4m53s
CI Code / Linux (arch) (push) Successful in 5m38s
Deprecate /logging command in favor of /history for chat logging control.
/history off now stops persistence (sets PREF_DBLOG=off + PREF_CHLOG=false)
in addition to hiding history on open. /history on restores persistence
(re-enabling PREF_DBLOG=on if it was off) and PREF_CHLOG.

statusbar
- PREF_STATUSBAR_SHOW_DBBACKEND (default ON) gates the [sqlite] /
  [flatfile] indicator; toggle via "/statusbar show|hide dbbackend"

/history off|on
- "/history off" now stops persistence as well as hiding history on
  open: sets PREF_DBLOG=off + PREF_CHLOG=false in addition to
  PREF_HISTORY=false
- "/history on" restores persistence (re-enabling PREF_DBLOG=on if
  it was off) and PREF_CHLOG, in addition to PREF_HISTORY=true

/logging
- Deprecated /logging command. It now prints a single notice
  pointing to /history; CMD_PREAMBLE trimmed (min_args=0, no
  setting_func, syntax/args/examples dropped); subcommand 'group'
  removed from autocomplete
- All "use '/logging chat on' to enable" hints replaced with
  "/history on" across /omemo-log, /pgp-log, /otr-log, and /ox-log

/privacy logging
- Single-pass validation across {on, off, redact, flatfile}
- Backend-switching values (on, flatfile) now take effect
  immediately when connected (via log_database_switch_backend),
  matching "/history switch" behaviour; off/redact only flip
  pref bits and keep the live backend open

/correction off
- win_print_outgoing and win_print_outgoing_with_receipt now gate
  _win_correct on PREF_CORRECTION_ALLOW, matching incoming-msg
  paths. Previously a peer's correction reflected via XEP-0280
  carbons (or the user's own /correct invocation) was applied
  in-buffer regardless of the pref

console
- Drop orphaned /logging chat reference from cons_privacy_setting()
- Delete cons_logging_setting() and remove its call from
  cons_show_log_prefs()

autocomplete
- Add dbbackend to statusbar_show_ac
- Remove logging_ac entries for chat/group subcommands
- Remove _logging_autocomplete() param handler for chat subcommand

database_flatfile
- Two local g_strndup allocations switched from char* with manual
  g_free to auto_gchar gchar* for automatic cleanup

tests
- Update test_cmd_otr.c to expect new /history-on warning messages

@author: jabber.developer2 <jabber.developer2@jabber.space>
2026-05-16 15:33:42 +00:00
12 changed files with 164 additions and 54 deletions

2
.gitignore vendored
View File

@@ -17,6 +17,8 @@ compile_commands.json
.project
.settings/
.vscode/
.kilo/
.roo/
*.plist/
# autotools

View File

@@ -186,14 +186,65 @@ ai_json_escape(const gchar* str)
return g_string_free(result, FALSE);
}
/**
* Convert a 4-hex-digit sequence at @p to a 16-bit code point.
* Returns -1 on invalid hex.
*/
static int
_hex4_to_int(const gchar* p)
{
int val = 0;
for (int i = 0; i < 4; i++) {
char c = p[i];
val <<= 4;
if (c >= '0' && c <= '9')
val |= c - '0';
else if (c >= 'a' && c <= 'f')
val |= c - 'a' + 10;
else if (c >= 'A' && c <= 'F')
val |= c - 'A' + 10;
else
return -1;
}
return val;
}
/**
* Write a UTF-8 encoding of @cp to @w, advancing @w.
* Returns the updated pointer.
*/
static gchar*
_write_utf8(gchar* w, unsigned int cp)
{
if (cp <= 0x7F) {
*w++ = (char)cp;
} else if (cp <= 0x7FF) {
*w++ = (char)(0xC0 | (cp >> 6));
*w++ = (char)(0x80 | (cp & 0x3F));
} else if (cp <= 0xFFFF) {
*w++ = (char)(0xE0 | (cp >> 12));
*w++ = (char)(0x80 | ((cp >> 6) & 0x3F));
*w++ = (char)(0x80 | (cp & 0x3F));
} else if (cp <= 0x10FFFF) {
*w++ = (char)(0xF0 | (cp >> 18));
*w++ = (char)(0x80 | ((cp >> 12) & 0x3F));
*w++ = (char)(0x80 | ((cp >> 6) & 0x3F));
*w++ = (char)(0x80 | (cp & 0x3F));
}
/* Invalid code point: silently skip */
return w;
}
/* Decode JSON string escapes in [start, end) into a freshly allocated buffer.
* Handles \" \\ \/ \n \t \r \b \f. \uXXXX sequences are passed through verbatim
* since the rest of the AI pipeline already deals in UTF-8 byte streams. */
* Handles \" \\ \/ \n \t \r \b \f and \uXXXX (including surrogate pairs). */
static gchar*
_json_unescape_substring(const gchar* start, const gchar* end)
{
gchar* out = g_new0(gchar, (end - start) + 1);
/* Estimate upper bound: each \uXXXX becomes up to 6 UTF-8 bytes. */
gsize max_len = (end - start) * 6 + 1;
gchar* out = g_new0(gchar, max_len);
gchar* w = out;
for (const gchar* in = start; in < end;) {
if (*in == '\\' && in + 1 < end) {
switch (in[1]) {
@@ -229,6 +280,64 @@ _json_unescape_substring(const gchar* start, const gchar* end)
*w++ = '\f';
in += 2;
break;
case 'u':
/* Handle \uXXXX or surrogate pair \uHHHH\uLLLL */
if (in + 6 > end) {
/* Not enough characters — emit literal */
*w++ = *in++;
break;
}
if (in[2] != '\\' || in[3] != 'u') {
/* Single \uXXXX */
int cp = _hex4_to_int(in + 2);
if (cp < 0) {
/* Invalid hex — emit literal backslash */
*w++ = *in++;
} else if (cp < 0xD800 || cp > 0xDFFF) {
/* Not a surrogate — encode directly */
w = _write_utf8(w, (unsigned int)cp);
in += 6;
} else {
/* High or low surrogate — expect pair */
unsigned int high = (unsigned int)cp;
if ((high >= 0xD800 && high <= 0xDBFF) && in + 12 <= end && in[6] == '\\' && in[7] == 'u') {
/* High surrogate followed by \u */
int low = _hex4_to_int(in + 8);
if (low >= 0xDC00 && low <= 0xDFFF) {
/* Valid surrogate pair */
unsigned int cp2 = (0x10000 + ((high & 0x3FF) << 10)) | (low & 0x3FF);
w = _write_utf8(w, cp2);
in += 12;
} else {
/* Invalid low surrogate — emit replacement */
w = _write_utf8(w, 0xFFFD);
in += 6;
}
} else {
/* Orphan high surrogate or lone low surrogate */
w = _write_utf8(w, 0xFFFD);
in += 6;
}
}
} else {
/* \uHHHH\uLLLL — surrogate pair */
int high = _hex4_to_int(in + 2);
if (high < 0) {
*w++ = *in++;
break;
}
int low = _hex4_to_int(in + 8);
if ((high >= 0xD800 && high <= 0xDBFF) && (low >= 0xDC00 && low <= 0xDFFF)) {
unsigned int cp2 = (0x10000 + ((unsigned int)(high & 0x3FF) << 10)) | (unsigned int)(low & 0x3FF);
w = _write_utf8(w, cp2);
in += 12;
} else {
/* Invalid pair — emit replacement for high */
w = _write_utf8(w, 0xFFFD);
in += 6;
}
}
break;
default:
*w++ = *in++;
break;
@@ -238,7 +347,9 @@ _json_unescape_substring(const gchar* start, const gchar* end)
}
}
*w = '\0';
return out;
/* Realloc to exact size */
return g_realloc(out, w - out + 1);
}
/* Locate "field":"...value..." in json and return the unescaped value.

View File

@@ -1660,7 +1660,7 @@ static const struct cmd_t command_defs[] = {
},
{ CMD_PREAMBLE("/logging",
parse_args, 0, 3, NULL)
parse_args, 0, 0, NULL)
CMD_MAINFUNC(cmd_logging)
CMD_TAGS(
CMD_TAG_CHAT)

View File

@@ -10932,21 +10932,14 @@ cmd_ai_start(ProfWin* window, const char* const command, gchar** args)
if (g_strv_length(args) >= 2) {
provider_name = args[1];
}
if (g_strv_length(args) >= 3) {
model = args[2];
if (!provider_name) {
cons_show_error("Please specify provider name.");
return TRUE;
}
// Resolve defaults
auto_gchar gchar* owned_provider_name = NULL;
if (!provider_name) {
// Use default provider from preferences
auto_gchar gchar* default_provider = prefs_get_string(PREF_AI_PROVIDER);
if (default_provider && strlen(default_provider) > 0) {
provider_name = default_provider;
}
}
if (!provider_name) {
provider_name = "openai"; // Fallback
if (g_strv_length(args) >= 3) {
model = args[2];
}
AIProvider* provider = ai_get_provider(provider_name);

View File

@@ -61,6 +61,22 @@ integrity_issue_free(integrity_issue_t* issue)
}
}
gboolean
log_database_can_recover_messages(void)
{
if (!active_db_backend) {
return FALSE;
}
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
if (g_strcmp0(pref_dblog, "off") == 0) {
return FALSE;
}
if (g_strcmp0(pref_dblog, "redact") == 0) {
return FALSE;
}
return TRUE;
}
gboolean
log_database_init(ProfAccount* account)
{

View File

@@ -109,6 +109,9 @@ ProfMessage* log_database_get_limits_info(const gchar* const contact_barejid, gb
void log_database_close(void);
GSList* log_database_verify_integrity(const gchar* const contact_barejid);
// FALSE if no backend, or PREF_DBLOG is "off" / "redact".
gboolean log_database_can_recover_messages(void);
// Cross-backend export/import (requires HAVE_SQLITE)
#ifdef HAVE_SQLITE
int log_database_export_to_flatfile(const gchar* const contact_jid);

View File

@@ -384,29 +384,16 @@ _sqlite_get_previous_chat(const gchar* const contact_barejid, const gchar* start
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);
// When LMC is disabled, return original and correction rows as separate
// messages instead of merging the correction text into the original.
// Otherwise the user sees corrected text in history even with
// /correction off.
const gboolean lmc_allowed = prefs_get_boolean(PREF_CORRECTION_ALLOW);
const char* msg_expr = lmc_allowed ? "COALESCE(B.`message`, A.`message`)" : "A.`message`";
const char* join_clause = lmc_allowed
? "LEFT JOIN `ChatLogs` AS B ON (A.`replaced_by_db_id` = B.`id` AND A.`from_jid` = B.`from_jid`) "
: "";
const char* replaces_filter = lmc_allowed ? "(A.`replaces_db_id` IS NULL) AND " : "";
auto_sqlite gchar* query = sqlite3_mprintf("SELECT * FROM ("
"SELECT %s AS message, "
"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 "
"%s"
"WHERE %s"
"((A.`from_jid` = %Q AND A.`to_jid` = %Q) OR (A.`from_jid` = %Q AND A.`to_jid` = %Q)) "
"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;",
msg_expr, join_clause, replaces_filter,
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);

View File

@@ -301,12 +301,6 @@ cl_ev_send_ai_msg(ProfAiWin* aiwin, const char* const message, const char* const
return;
}
/* Reset paged flag before printing user message.
* If the user scrolled up to view history, paged=1 would suppress
* the message in _win_printf(). Reset it here so the message displays. */
aiwin->window.layout->paged = 0;
aiwin->window.layout->unread_msg = 0;
// Display user message in AI window.
win_print_outgoing(&aiwin->window, ">>", id, NULL, message);

View File

@@ -1948,22 +1948,12 @@ cons_log_setting(void)
cons_show("Log level (/log level) : %s", level);
}
void
cons_logging_setting(void)
{
if (prefs_get_boolean(PREF_GRLOG))
cons_show("Groupchat logging (/logging group) : ON");
else
cons_show("Groupchat logging (/logging group) : OFF");
}
void
cons_show_log_prefs(void)
{
cons_show("Logging preferences:");
cons_show("");
cons_log_setting();
cons_logging_setting();
cons_alert(NULL);
}

View File

@@ -46,6 +46,7 @@
#include "ui/ui.h"
#include "ui/titlebar.h"
#include "ui/inputwin.h"
#include "ui/win_types.h"
#include "ui/window_list.h"
#include "ui/window.h"
#include "ui/screen.h"
@@ -252,6 +253,10 @@ _title_bar_draw(void)
_show_muc_privacy(mucwin);
_show_attention(current, mucwin->has_attention);
_show_scrolled(current);
} else if (current && current->type == WIN_AI) {
ProfAiWin* aiwin = (ProfAiWin*)current;
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
_show_scrolled(current);
}
_show_self_presence();

View File

@@ -330,7 +330,6 @@ void cons_history_setting(void);
void cons_carbons_setting(void);
void cons_receipts_setting(void);
void cons_log_setting(void);
void cons_logging_setting(void);
void cons_autoaway_setting(void);
void cons_reconnect_setting(void);
void cons_autoping_setting(void);

View File

@@ -848,6 +848,14 @@ win_page_down(ProfWin* window, int scroll_size)
window->layout->paged = 0;
window->layout->unread_msg = 0;
}
// Non-chat windows have no DB to fetch from, so WIN_SCROLL_REACHED_BOTTOM
// never fires; reset paged once the buffer's last line is on screen.
if (window->type != WIN_CHAT
&& (total_rows - *page_start) <= page_space + 1) {
window->layout->paged = 0;
window->layout->unread_msg = 0;
}
}
void
@@ -1801,10 +1809,12 @@ static void
_win_printf(ProfWin* window, const char* show_char, int pad_indent, GDateTime* timestamp, int flags, theme_item_t theme_item, const char* const display_from, const char* const from_jid, const char* const message_id, const char* const message, ...)
{
/* Prevent printing and buffer update when user is viewing message history [SCROLLING]*/
if (window->layout->paged && wins_is_current(window)) {
window->layout->unread_msg++;
return;
// Skip buffer only when DB can replay the message on scroll-down.
if (window->type == WIN_CHAT && log_database_can_recover_messages()) {
return;
}
}
if (timestamp == NULL) {