bug: escape LIKE wildcard chars in search queries (#145)
User search queries were wrapped in `%...%` LIKE patterns without escaping SQL LIKE wildcards, causing `%` searches to return every message in the database. This adds an `escapeLIKE` helper that escapes `\`, `%`, and `_` before wrapping them, adding `ESCAPE '\'` clauses to all LIKE predicates to ensure user input is treated as literal text. Closes #56
This commit is contained in:
parent
7a2b323098
commit
ff2d74de9b
@ -30,13 +30,23 @@ func (d *DB) SearchMessages(p SearchMessagesParams) ([]Message, error) {
|
||||
return d.searchLIKE(p)
|
||||
}
|
||||
|
||||
// escapeLIKE escapes SQL LIKE wildcard characters (%, _) and the escape
|
||||
// character itself so that user input is treated as a literal string (#56).
|
||||
func escapeLIKE(s string) string {
|
||||
s = strings.ReplaceAll(s, `\`, `\\`)
|
||||
s = strings.ReplaceAll(s, `%`, `\%`)
|
||||
s = strings.ReplaceAll(s, `_`, `\_`)
|
||||
return s
|
||||
}
|
||||
|
||||
func (d *DB) searchLIKE(p SearchMessagesParams) ([]Message, error) {
|
||||
query := `
|
||||
SELECT m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.display_text,''), COALESCE(m.media_type,''), ''
|
||||
FROM messages m
|
||||
LEFT JOIN chats c ON c.jid = m.chat_jid
|
||||
WHERE (LOWER(m.text) LIKE LOWER(?) OR LOWER(m.display_text) LIKE LOWER(?) OR LOWER(m.media_caption) LIKE LOWER(?) OR LOWER(m.filename) LIKE LOWER(?) OR LOWER(COALESCE(m.chat_name,'')) LIKE LOWER(?) OR LOWER(COALESCE(m.sender_name,'')) LIKE LOWER(?) OR LOWER(COALESCE(c.name,'')) LIKE LOWER(?))`
|
||||
needle := "%" + p.Query + "%"
|
||||
WHERE (LOWER(m.text) LIKE LOWER(?) ESCAPE '\' OR LOWER(m.display_text) LIKE LOWER(?) ESCAPE '\' OR LOWER(m.media_caption) LIKE LOWER(?) ESCAPE '\' OR LOWER(m.filename) LIKE LOWER(?) ESCAPE '\' OR LOWER(COALESCE(m.chat_name,'')) LIKE LOWER(?) ESCAPE '\' OR LOWER(COALESCE(m.sender_name,'')) LIKE LOWER(?) ESCAPE '\' OR LOWER(COALESCE(c.name,'')) LIKE LOWER(?) ESCAPE '\')`
|
||||
// Escape wildcards before wrapping in % so user input is literal (#56).
|
||||
needle := "%" + escapeLIKE(p.Query) + "%"
|
||||
args := []interface{}{needle, needle, needle, needle, needle, needle, needle}
|
||||
query, args = applyMessageFilters(query, args, p)
|
||||
query += " ORDER BY m.ts DESC LIMIT ?"
|
||||
|
||||
@ -41,3 +41,48 @@ func TestSearchMessagesUsesLIKEWhenFTSDisabled(t *testing.T) {
|
||||
t.Fatalf("expected empty snippet for LIKE search, got %q", ms[0].Snippet)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSearchLIKEWildcardEscape verifies that LIKE wildcard characters in user
|
||||
// queries are treated as literals, not SQL pattern chars (#56).
|
||||
func TestSearchLIKEWildcardEscape(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
chat := "555@s.whatsapp.net"
|
||||
if err := db.UpsertChat(chat, "dm", "Bob", time.Now()); err != nil {
|
||||
t.Fatalf("UpsertChat: %v", err)
|
||||
}
|
||||
msgs := []struct{ id, text string }{
|
||||
{"m1", "hello world"},
|
||||
{"m2", "100% sure"},
|
||||
{"m3", "some_thing here"},
|
||||
{"m4", "another message"},
|
||||
}
|
||||
for _, m := range msgs {
|
||||
if err := db.UpsertMessage(UpsertMessageParams{
|
||||
ChatJID: chat, MsgID: m.id, Timestamp: time.Now(), Text: m.text,
|
||||
}); err != nil {
|
||||
t.Fatalf("UpsertMessage %s: %v", m.id, err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("percent returns only exact match", func(t *testing.T) {
|
||||
// Without escaping, '%' would match everything.
|
||||
ms, err := db.SearchMessages(SearchMessagesParams{Query: "100%", Limit: 50})
|
||||
if err != nil {
|
||||
t.Fatalf("SearchMessages: %v", err)
|
||||
}
|
||||
if len(ms) != 1 || ms[0].MsgID != "m2" {
|
||||
t.Fatalf("expected only m2, got %d results: %v", len(ms), ms)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("underscore returns only exact match", func(t *testing.T) {
|
||||
// Without escaping, '_' would match any single character.
|
||||
ms, err := db.SearchMessages(SearchMessagesParams{Query: "some_thing", Limit: 50})
|
||||
if err != nil {
|
||||
t.Fatalf("SearchMessages: %v", err)
|
||||
}
|
||||
if len(ms) != 1 || ms[0].MsgID != "m3" {
|
||||
t.Fatalf("expected only m3, got %d results: %v", len(ms), ms)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user