isprivate() reads the GTK input purpose and nothing else, but the purpose is not the only way an application marks a password field, and it is not the older one. GTK 3.24.52, measured: gtk_entry_set_visibility(entry, FALSE) purpose stays FREE_FORM gtk_entry_set_input_purpose(PASSWORD) purpose PASSWORD so an entry hidden the first way looks like ordinary text to the module. This machine runs one. /usr/libexec/xfce-polkit, the XFCE authentication dialog, is up right now under GTK_IM_MODULE=strans, and its binary calls gtk_entry_set_visibility and never gtk_entry_set_input_purpose, so every password typed into it goes through the engine. Against the real daemon, in Korean mode: typed hunter2 stored ㅗㅕ숟ㄱ2 typed correcthorse stored 책ㄱㄷㅊ쇅ㄴㄷ typed P4ssw0rd stored ㅔ4ㄴㄵ0ㄱㅇ The field draws bullets, so nothing on screen says why the authentication failed -- except the pending syllable, which is drawn as itself: three keys into such an entry the old module leaves 한 on screen where the field should read ●●●. With this it reads ●●●, and the entry holds gks. set_visibility sends the input context no signal, so the question cannot be answered where the purpose is, in init and notify::input-purpose. It has to be asked at the key, of the client window, which is the entry's own: gdk_window_get_user_data on it returns the GtkEntry. fcitx5-gtk asks it the same way in all three of its GTK versions -- gtk3/fcitximcontext .cpp:1155-1162, under the comment "seems visibility != PASSWORD hint". It costs one field read and one type check per key, on a path that then does a socket round trip anyway. It does not cover XIM: that protocol has no attribute for this, and no fcitx5 frontend answers it either -- only its GTK and Qt client modules do -- so an X client reached over XIM still composes in its password field. Nor does it notice a "show password" box switched off in the middle of a composition: there is no signal for visibility, so the pending text stays in the daemon until the next focus change. Noticing that needs a signal connection on a widget the module does not own, which is a bigger thing than the hole is. The test is a live one and it earns its line: with the fix removed it says gtk_live_test: hidden-entry key reached daemon or did not commit Both guards were checked by removing them. Without GTK_IS_ENTRY the run takes a Gtk-CRITICAL and fails; without the im->win test it exits 139, on the stalled-peer context, which never gets a client window. An ishidden that always answers yes fails earlier still, at the initial protocol frames. Breaking the test's own helper so it hands back the toplevel's window instead of the entry's fails too, which is what says the assertion watches the right window rather than merely counting no events. That count is the one thing that had to be made deterministic. A new client window invalidates the caret, and the fake daemon records that frame on its own thread, so sampling the event count straight after set_client_window raced it: 2 failures in 8 runs. The block now waits for that frame and names it, and there were 0 in 20 after. 90 unit, check-live, check-stress and valgrind clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
549 lines
11 KiB
C
549 lines
11 KiB
C
#include <errno.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <gtk/gtk.h>
|
|
#ifdef GDK_WINDOWING_X11
|
|
#include <gdk/gdkx.h>
|
|
#endif
|
|
#include "ipc.h"
|
|
|
|
typedef struct Im Im;
|
|
struct Im
|
|
{
|
|
GtkIMContextSimple parent;
|
|
int fd;
|
|
int usepreedit;
|
|
int ext;
|
|
int private;
|
|
int simpleactive;
|
|
int simpledone;
|
|
char pre[Ipcfieldmax+1];
|
|
int prelen;
|
|
GdkWindow *win;
|
|
GdkRectangle cursor;
|
|
int cursorvalid;
|
|
int caretsent;
|
|
unsigned char sent[Ipccaretsz];
|
|
};
|
|
|
|
typedef struct ImClass ImClass;
|
|
struct ImClass
|
|
{
|
|
GtkIMContextSimpleClass parent;
|
|
};
|
|
|
|
static GType imtype;
|
|
static GObjectClass *parentobject;
|
|
static GtkIMContextClass *parentim;
|
|
|
|
static void
|
|
setpreedit(Im *im, const char *s, int n)
|
|
{
|
|
int was;
|
|
|
|
if(n < 0 || n > Ipcfieldmax)
|
|
n = 0;
|
|
if(n == 0 && im->prelen == 0)
|
|
return;
|
|
was = im->prelen;
|
|
if(n > 0)
|
|
memcpy(im->pre, s, n);
|
|
im->pre[n] = '\0';
|
|
im->prelen = n;
|
|
if(was == 0 && n > 0)
|
|
g_signal_emit_by_name(im, "preedit-start");
|
|
g_signal_emit_by_name(im, "preedit-changed");
|
|
if(was > 0 && n == 0)
|
|
g_signal_emit_by_name(im, "preedit-end");
|
|
}
|
|
|
|
static void
|
|
srvdrop(Im *im, int notify)
|
|
{
|
|
if(im->fd >= 0)
|
|
close(im->fd);
|
|
im->fd = -1;
|
|
im->ext = 0;
|
|
im->caretsent = 0;
|
|
if(notify)
|
|
setpreedit(im, "", 0);
|
|
else{
|
|
im->pre[0] = '\0';
|
|
im->prelen = 0;
|
|
}
|
|
}
|
|
|
|
static void
|
|
srvclose(Im *im)
|
|
{
|
|
srvdrop(im, 1);
|
|
}
|
|
|
|
/* Both buffers are char[Ipcfieldmax+1]. */
|
|
static int
|
|
readresp(Im *im, int want, char *commit, char *pre, Ipcresp *resp)
|
|
{
|
|
if(ipcreadresp(im->fd, want, commit, pre, resp) < 0)
|
|
return -1;
|
|
if(!g_utf8_validate(commit, resp->commitlen, NULL) ||
|
|
(want && !g_utf8_validate(pre, resp->preeditlen, NULL))){
|
|
errno = EPROTO;
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
dropwindow(Im *im)
|
|
{
|
|
if(im->win != NULL)
|
|
g_object_unref(im->win);
|
|
im->win = NULL;
|
|
im->cursorvalid = 0;
|
|
}
|
|
|
|
/* Root-relative caret in device pixels; returns whether it is known. */
|
|
static int
|
|
caretget(Im *im, int32_t *x, int32_t *y, int32_t *h)
|
|
{
|
|
gint rx, ry, scale;
|
|
int64_t sx, sy, sh;
|
|
|
|
*x = *y = *h = 0;
|
|
if(im->win != NULL && gdk_window_is_destroyed(im->win))
|
|
dropwindow(im);
|
|
if(!im->cursorvalid || im->win == NULL || im->cursor.height < 0)
|
|
return 0;
|
|
#ifdef GDK_WINDOWING_X11
|
|
if(!GDK_IS_X11_WINDOW(im->win))
|
|
return 0;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
scale = gdk_window_get_scale_factor(im->win);
|
|
if(scale <= 0)
|
|
return 0;
|
|
/* GDK translates the client-relative point before device scaling. */
|
|
gdk_window_get_root_coords(im->win, 0, 0, &rx, &ry);
|
|
sx = ((int64_t)rx + im->cursor.x) * scale;
|
|
sy = ((int64_t)ry + im->cursor.y) * scale;
|
|
sh = (int64_t)im->cursor.height * scale;
|
|
if(sx < INT32_MIN || sx > INT32_MAX ||
|
|
sy < INT32_MIN || sy > INT32_MAX || sh > INT32_MAX)
|
|
return 0;
|
|
*x = sx;
|
|
*y = sy;
|
|
*h = sh;
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
sendcaret(Im *im)
|
|
{
|
|
unsigned char buf[Ipccaretsz];
|
|
int32_t x, y, h;
|
|
int valid;
|
|
|
|
if(im->fd < 0 || !im->ext)
|
|
return 0;
|
|
valid = caretget(im, &x, &y, &h);
|
|
ipcpackcaret(buf, valid, x, y, h);
|
|
if(im->caretsent && memcmp(buf, im->sent, sizeof buf) == 0)
|
|
return 0;
|
|
if(ipcsend(im->fd, buf, sizeof buf) < 0)
|
|
return -1;
|
|
memcpy(im->sent, buf, sizeof buf);
|
|
im->caretsent = 1;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
srvconnect(Im *im)
|
|
{
|
|
unsigned char buf[Ipcreqsz];
|
|
char commit[Ipcfieldmax+1];
|
|
char pre[Ipcfieldmax+1];
|
|
Ipcresp resp;
|
|
|
|
if(im->fd >= 0)
|
|
return 0;
|
|
im->fd = ipcconnect();
|
|
if(im->fd < 0)
|
|
return -1;
|
|
/* Marker zero is an old daemon's harmless key-zero response. */
|
|
ipcpackcap(buf, im->usepreedit);
|
|
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
|
|
readresp(im, im->usepreedit, commit, pre, &resp) < 0){
|
|
srvclose(im);
|
|
return -1;
|
|
}
|
|
im->ext = resp.eaten != 0;
|
|
if(sendcaret(im) < 0){
|
|
srvclose(im);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
simplecommit(GtkIMContext *ctx, const char *s, Im *im)
|
|
{
|
|
(void)ctx;
|
|
(void)s;
|
|
im->simpledone = 1;
|
|
}
|
|
|
|
static void
|
|
simpleend(GtkIMContext *ctx, Im *im)
|
|
{
|
|
(void)ctx;
|
|
im->simpledone = 1;
|
|
}
|
|
|
|
/*
|
|
* GtkIMContextSimple composes dead keys and Compose sequences; it keeps
|
|
* the key stream until it commits, ends its preedit, or rejects a press.
|
|
*/
|
|
static gboolean
|
|
simplefilter(GtkIMContext *ctx, GdkEventKey *ev, int release)
|
|
{
|
|
Im *im;
|
|
gboolean r;
|
|
|
|
im = (Im*)ctx;
|
|
im->simpleactive = 1;
|
|
im->simpledone = 0;
|
|
r = parentim->filter_keypress(ctx, ev);
|
|
if((!r && !release) || im->simpledone)
|
|
im->simpleactive = 0;
|
|
return r;
|
|
}
|
|
|
|
/* The daemon hands the pending text back; it becomes committed text. */
|
|
static void
|
|
sendreset(Im *im)
|
|
{
|
|
unsigned char buf[Ipcreqsz];
|
|
char commit[Ipcfieldmax+1];
|
|
char pre[Ipcfieldmax+1];
|
|
Ipcresp resp;
|
|
|
|
if(im->fd < 0){
|
|
setpreedit(im, "", 0);
|
|
return;
|
|
}
|
|
ipcpackreset(buf, im->usepreedit);
|
|
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
|
|
readresp(im, im->usepreedit, commit, pre, &resp) < 0){
|
|
srvclose(im);
|
|
return;
|
|
}
|
|
setpreedit(im, "", 0);
|
|
if(commit[0] != '\0')
|
|
g_signal_emit_by_name(im, "commit", commit);
|
|
}
|
|
|
|
/* A password entry hidden the older way sets no purpose and signals nothing. */
|
|
static int
|
|
ishidden(Im *im)
|
|
{
|
|
GtkWidget *w;
|
|
|
|
if(im->win == NULL)
|
|
return 0;
|
|
gdk_window_get_user_data(im->win, (gpointer*)&w);
|
|
return GTK_IS_ENTRY(w) && !gtk_entry_get_visibility(GTK_ENTRY(w));
|
|
}
|
|
|
|
static gboolean
|
|
kpress(GtkIMContext *ctx, GdkEventKey *ev)
|
|
{
|
|
Im *im;
|
|
unsigned char buf[Ipcreqsz];
|
|
char commit[Ipcfieldmax+1];
|
|
char pre[Ipcfieldmax+1];
|
|
uint32_t key, mod;
|
|
Ipcresp resp;
|
|
gboolean r;
|
|
|
|
im = (Im*)ctx;
|
|
if(ev->type != GDK_KEY_PRESS){
|
|
if(im->simpleactive)
|
|
return simplefilter(ctx, ev, 1);
|
|
return FALSE;
|
|
}
|
|
if(im->simpleactive)
|
|
return simplefilter(ctx, ev, 0);
|
|
key = ipckeysym(ev->keyval, gdk_keyval_to_unicode(ev->keyval));
|
|
mod = ipcmod(ev->state);
|
|
if(im->private || ishidden(im) || key == 0){
|
|
r = simplefilter(ctx, ev, 0);
|
|
/* A dead key starts a compose: pending text was typed first. */
|
|
if(im->simpleactive)
|
|
sendreset(im);
|
|
return r;
|
|
}
|
|
if(srvconnect(im) < 0)
|
|
return simplefilter(ctx, ev, 0);
|
|
/* A retained GDK window may have moved since the last cursor report. */
|
|
if(sendcaret(im) < 0){
|
|
srvclose(im);
|
|
return simplefilter(ctx, ev, 0);
|
|
}
|
|
ipcpackreq(buf, im->usepreedit, mod, key);
|
|
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
|
|
readresp(im, im->usepreedit, commit, pre, &resp) < 0){
|
|
srvclose(im);
|
|
return simplefilter(ctx, ev, 0);
|
|
}
|
|
if(im->usepreedit && commit[0] != '\0' && im->prelen > 0)
|
|
setpreedit(im, "", 0);
|
|
if(commit[0] != '\0')
|
|
g_signal_emit_by_name(ctx, "commit", commit);
|
|
if(im->usepreedit)
|
|
setpreedit(im, pre, resp.preeditlen);
|
|
if(resp.eaten)
|
|
return TRUE;
|
|
return simplefilter(ctx, ev, 0);
|
|
}
|
|
|
|
static void
|
|
getpreedit(GtkIMContext *ctx, gchar **str, PangoAttrList **attrs,
|
|
gint *cursor_pos)
|
|
{
|
|
Im *im;
|
|
PangoAttribute *u;
|
|
|
|
im = (Im*)ctx;
|
|
if(im->simpleactive){
|
|
parentim->get_preedit_string(ctx, str, attrs, cursor_pos);
|
|
return;
|
|
}
|
|
if(str)
|
|
*str = g_strdup(im->pre);
|
|
if(attrs){
|
|
*attrs = pango_attr_list_new();
|
|
if(im->prelen > 0){
|
|
u = pango_attr_underline_new(PANGO_UNDERLINE_SINGLE);
|
|
u->start_index = 0;
|
|
u->end_index = im->prelen;
|
|
pango_attr_list_insert(*attrs, u);
|
|
}
|
|
}
|
|
if(cursor_pos)
|
|
*cursor_pos = g_utf8_strlen(im->pre, -1);
|
|
}
|
|
|
|
static void
|
|
reset(GtkIMContext *ctx)
|
|
{
|
|
if(parentim->reset != NULL)
|
|
parentim->reset(ctx);
|
|
((Im*)ctx)->simpleactive = 0;
|
|
sendreset((Im*)ctx);
|
|
}
|
|
|
|
static void
|
|
focusout(GtkIMContext *ctx)
|
|
{
|
|
Im *im;
|
|
|
|
im = (Im*)ctx;
|
|
if(parentim->focus_out != NULL)
|
|
parentim->focus_out(ctx);
|
|
im->simpleactive = 0;
|
|
/* Closing the connection releases the engine. */
|
|
sendreset(im);
|
|
srvclose(im);
|
|
}
|
|
|
|
static void
|
|
setusepreedit(GtkIMContext *ctx, gboolean use)
|
|
{
|
|
Im *im;
|
|
unsigned char buf[Ipcreqsz];
|
|
char commit[Ipcfieldmax+1];
|
|
char pre[Ipcfieldmax+1];
|
|
Ipcresp resp;
|
|
|
|
im = (Im*)ctx;
|
|
use = use != FALSE;
|
|
if(parentim->set_use_preedit != NULL)
|
|
parentim->set_use_preedit(ctx, use);
|
|
if(im->usepreedit == use)
|
|
return;
|
|
im->usepreedit = use;
|
|
if(!use)
|
|
setpreedit(im, "", 0);
|
|
if(im->fd < 0 || !im->ext)
|
|
return;
|
|
ipcpackcap(buf, use);
|
|
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
|
|
readresp(im, use, commit, pre, &resp) < 0 ||
|
|
!resp.eaten){
|
|
srvclose(im);
|
|
return;
|
|
}
|
|
if(use)
|
|
setpreedit(im, pre, resp.preeditlen);
|
|
}
|
|
|
|
static void
|
|
setclientwindow(GtkIMContext *ctx, GdkWindow *win)
|
|
{
|
|
Im *im;
|
|
|
|
im = (Im*)ctx;
|
|
if(win != NULL && gdk_window_is_destroyed(win))
|
|
win = NULL;
|
|
if(parentim->set_client_window != NULL)
|
|
parentim->set_client_window(ctx, win);
|
|
if(im->win == win)
|
|
return;
|
|
if(win != NULL)
|
|
g_object_ref(win);
|
|
dropwindow(im);
|
|
im->win = win;
|
|
if(sendcaret(im) < 0)
|
|
srvclose(im);
|
|
}
|
|
|
|
static void
|
|
setcursorlocation(GtkIMContext *ctx, GdkRectangle *area)
|
|
{
|
|
Im *im;
|
|
|
|
im = (Im*)ctx;
|
|
if(parentim->set_cursor_location != NULL)
|
|
parentim->set_cursor_location(ctx, area);
|
|
if(im->win != NULL && gdk_window_is_destroyed(im->win))
|
|
dropwindow(im);
|
|
if(area == NULL || im->win == NULL)
|
|
im->cursorvalid = 0;
|
|
else{
|
|
im->cursor = *area;
|
|
im->cursorvalid = 1;
|
|
}
|
|
if(sendcaret(im) < 0)
|
|
srvclose(im);
|
|
}
|
|
|
|
static int
|
|
isprivate(GtkInputPurpose purpose)
|
|
{
|
|
return purpose == GTK_INPUT_PURPOSE_PASSWORD ||
|
|
purpose == GTK_INPUT_PURPOSE_PIN;
|
|
}
|
|
|
|
static void
|
|
purposechanged(GObject *obj, GParamSpec *pspec, gpointer data)
|
|
{
|
|
Im *im;
|
|
GtkInputPurpose purpose;
|
|
int private;
|
|
|
|
(void)pspec;
|
|
(void)data;
|
|
im = (Im*)obj;
|
|
g_object_get(obj, "input-purpose", &purpose, NULL);
|
|
private = isprivate(purpose);
|
|
if(private == im->private)
|
|
return;
|
|
im->private = private;
|
|
if(private)
|
|
reset(GTK_IM_CONTEXT(im));
|
|
}
|
|
|
|
static void
|
|
dispose(GObject *obj)
|
|
{
|
|
Im *im;
|
|
|
|
im = (Im*)obj;
|
|
srvdrop(im, 0);
|
|
dropwindow(im);
|
|
parentobject->dispose(obj);
|
|
}
|
|
|
|
static void
|
|
init(Im *im)
|
|
{
|
|
GtkInputPurpose purpose;
|
|
|
|
im->fd = -1;
|
|
im->usepreedit = 1;
|
|
im->pre[0] = '\0';
|
|
g_object_get(im, "input-purpose", &purpose, NULL);
|
|
im->private = isprivate(purpose);
|
|
g_signal_connect(im, "notify::input-purpose",
|
|
G_CALLBACK(purposechanged), NULL);
|
|
g_signal_connect(im, "commit", G_CALLBACK(simplecommit), im);
|
|
g_signal_connect(im, "preedit-end", G_CALLBACK(simpleend), im);
|
|
}
|
|
|
|
static void
|
|
classinit(ImClass *klass)
|
|
{
|
|
GtkIMContextClass *ic;
|
|
GObjectClass *oc;
|
|
|
|
ic = GTK_IM_CONTEXT_CLASS(klass);
|
|
oc = G_OBJECT_CLASS(klass);
|
|
parentim = g_type_class_peek_parent(klass);
|
|
parentobject = G_OBJECT_CLASS(parentim);
|
|
ic->filter_keypress = kpress;
|
|
ic->get_preedit_string = getpreedit;
|
|
ic->reset = reset;
|
|
ic->focus_out = focusout;
|
|
ic->set_use_preedit = setusepreedit;
|
|
ic->set_client_window = setclientwindow;
|
|
ic->set_cursor_location = setcursorlocation;
|
|
oc->dispose = dispose;
|
|
}
|
|
|
|
static const GtkIMContextInfo info = {
|
|
"strans",
|
|
"strans",
|
|
"strans",
|
|
"",
|
|
"*",
|
|
};
|
|
|
|
static const GtkIMContextInfo *infolist[] = { &info };
|
|
|
|
G_MODULE_EXPORT void
|
|
im_module_init(GTypeModule *mod)
|
|
{
|
|
static const GTypeInfo ti = {
|
|
sizeof(ImClass),
|
|
NULL, NULL,
|
|
(GClassInitFunc)classinit,
|
|
NULL, NULL,
|
|
sizeof(Im),
|
|
0,
|
|
(GInstanceInitFunc)init,
|
|
};
|
|
imtype = g_type_module_register_type(mod, GTK_TYPE_IM_CONTEXT_SIMPLE,
|
|
"strans-gtk", &ti, 0);
|
|
}
|
|
|
|
G_MODULE_EXPORT void
|
|
im_module_exit(void)
|
|
{
|
|
}
|
|
|
|
G_MODULE_EXPORT void
|
|
im_module_list(const GtkIMContextInfo ***contexts, int *n)
|
|
{
|
|
*contexts = infolist;
|
|
*n = 1;
|
|
}
|
|
|
|
G_MODULE_EXPORT GtkIMContext*
|
|
im_module_create(const char *id)
|
|
{
|
|
if(strcmp(id, "strans") == 0)
|
|
return g_object_new(imtype, NULL);
|
|
return NULL;
|
|
}
|