/* dialog.c --- the password dialog and splash screen. * xscreensaver, Copyright © 1993-2021 Jamie Zawinski * * Permission to use, copy, modify, distribute, and sell this software and its * documentation for any purpose is hereby granted without fee, provided that * the above copyright notice appear in all copies and that both that * copyright notice and this permission notice appear in supporting * documentation. No representations are made about the suitability of this * software for any purpose. It is provided "as is" without express or * implied warranty. */ /* This file renders the unlock dialog and splash screen, using Xlib and Xft. * One significant complication is that it must read raw XInput2 events to * get keyboard and mouse input, as the "xscreensaver" process has the mouse * and keyboard grabbed while this is running. * * It might be possible to implement this file using Gtk instead of Xlib, * but the grab situation might make that tricky: those events would have to * be re-sent to the toolkit widgets in a way that it would understand them. * Also, toolkits tend to assume that a window manager exists, and this * window must be an OverrideRedirect window with no focus management. * * Crashes here are interpreted as "unauthorized" and do not unlock the * screen. */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include #include #include #include #include #include #ifdef HAVE_UNISTD_H # include #endif #ifdef HAVE_UNAME # include #endif /* HAVE_UNAME */ #include #include #include /* for CARD32 */ #include #include #include #include #ifdef ENABLE_NLS # include # include # define _(S) gettext(S) #else # define _(S) (S) #endif #ifdef HAVE_XKB # include # include #endif #include "version.h" #include "blurb.h" #include "auth.h" #include "atoms.h" #include "screens.h" #include "xft.h" #include "xftwrap.h" #include "xinput.h" #include "resources.h" #include "visual.h" #include "font-retry.h" #include "prefs.h" #include "usleep.h" extern Bool debug_p; #undef DEBUG_METRICS #undef DEBUG_STACKING #define LOCK_FAILURE_ATOM "_XSCREENSAVER_AUTH_FAILURES" #undef MAX #undef MIN #define MAX(a,b) ((a)>(b)?(a):(b)) #define MIN(a,b) ((a)<(b)?(a):(b)) #define MAX_BYTES_PER_CHAR 8 /* UTF-8 uses up to 6 bytes */ #define MAX_PASSWD_CHARS 280 /* Longest possible passphrase */ typedef struct window_state window_state; typedef enum { AUTH_READ, /* reading input or ready to do so */ AUTH_SUCCESS, /* auth success, unlock */ AUTH_FAIL, /* auth fail */ AUTH_CANCEL, /* user canceled, or typed blank password */ AUTH_TIME, /* timed out */ AUTH_FINISHED, /* user pressed enter */ AUTH_NOTIFY /* displaying message after finished */ } auth_state; /* A mini-toolkit for rendering text labels, input fields, and buttons. */ typedef enum { CENTER, LEFT, RIGHT } line_align; typedef struct { Bool down_p; XRectangle rect; char *cmd; void (*fn) (window_state *ws); Bool disabled_p; } line_button_state; typedef struct { char *text; XftFont *font; XftColor fg; Pixel bg; enum { LABEL, BUTTON, TEXT, TEXT_RO } type; line_align align; Bool float_p; Bool i_beam; line_button_state *button; } dialog_line; /* Global state. */ struct window_state { XtAppContext app; Display *dpy; Screen *screen; Position cx, cy, x, y; Dimension min_height; Window window; Colormap cmap; Bool splash_p; auth_state auth_state; int xi_opcode; int xkb_opcode; /* Variant strings */ char *version; char *user; int nmsgs; const auth_message *msgs; /* "Characters" in the password may be a variable number of bytes long. plaintext_passwd contains the raw bytes. plaintext_passwd_char_size indicates the size in bytes of each character, so that we can make backspace work. censored_passwd is the asterisk version. Maybe it would be more sensible to use uint32_t and utils/utf8wc.c here, but the multi-byte string returned by XLookupString might not be UTF-8 (see comment in handle_keypress). */ char plaintext_passwd [MAX_PASSWD_CHARS * MAX_BYTES_PER_CHAR]; char censored_passwd [MAX_PASSWD_CHARS * MAX_BYTES_PER_CHAR]; char plaintext_passwd_char_size [MAX_PASSWD_CHARS]; XComposeStatus compose_status; XtIntervalId timer; XtIntervalId cursor_timer; /* Blink the I-beam */ int i_beam; double start_time, end_time; Bool show_stars_p; /* "I regret that I have but one asterisk for my country." -- Nathan Hale, 1776. */ Bool caps_p; /* Whether we saw a keypress with caps-lock on */ char *dialog_theme; char *heading_label; char *body_label; char *hostname_label; char *date_format; char *kbd_layout_label; char *newlogin_cmd; /* Resources for fonts and colors */ XftDraw *xftdraw; XftFont *heading_font; XftFont *body_font; XftFont *error_font; XftFont *label_font; XftFont *date_font; XftFont *button_font; XftFont *hostname_font; Pixel foreground; Pixel background; XftColor xft_foreground; XftColor xft_text_foreground; XftColor xft_button_foreground; XftColor xft_error_foreground; Pixel passwd_background; Pixel thermo_foreground; Pixel thermo_background; Pixel shadow_top; Pixel shadow_bottom; Pixel border_color; Pixel button_background; Pixel logo_background; Dimension preferred_logo_width; Dimension preferred_logo_height; Dimension thermo_width; Dimension internal_padding; Dimension shadow_width; Dimension border_width; Pixmap logo_pixmap; Pixmap logo_clipmask; unsigned int logo_width, logo_height; int logo_npixels; unsigned long *logo_pixels; line_button_state newlogin_button_state; line_button_state unlock_button_state; line_button_state demo_button_state; line_button_state help_button_state; }; static void draw_shaded_rectangle (Display *dpy, Window window, int x, int y, int width, int height, int thickness, unsigned long top_color, unsigned long bottom_color) { XPoint points[4]; XGCValues gcv; GC gc1, gc2; if (thickness == 0) return; gcv.foreground = top_color; gc1 = XCreateGC (dpy, window, GCForeground, &gcv); gcv.foreground = bottom_color; gc2 = XCreateGC (dpy, window, GCForeground, &gcv); points [0].x = x; points [0].y = y; points [1].x = x + width; points [1].y = y; points [2].x = x + width - thickness; points [2].y = y + thickness; points [3].x = x; points [3].y = y + thickness; XFillPolygon (dpy, window, gc1, points, 4, Convex, CoordModeOrigin); points [0].x = x; points [0].y = y + thickness; points [1].x = x; points [1].y = y + height; points [2].x = x + thickness; points [2].y = y + height - thickness; points [3].x = x + thickness; points [3].y = y + thickness; XFillPolygon (dpy, window, gc1, points, 4, Convex, CoordModeOrigin); points [0].x = x + width; points [0].y = y; points [1].x = x + width - thickness; points [1].y = y + thickness; points [2].x = x + width - thickness; points [2].y = y + height - thickness; points [3].x = x + width; points [3].y = y + height - thickness; XFillPolygon (dpy, window, gc2, points, 4, Convex, CoordModeOrigin); points [0].x = x; points [0].y = y + height; points [1].x = x + width; points [1].y = y + height; points [2].x = x + width; points [2].y = y + height - thickness; points [3].x = x + thickness; points [3].y = y + height - thickness; XFillPolygon (dpy, window, gc2, points, 4, Convex, CoordModeOrigin); XFreeGC (dpy, gc1); XFreeGC (dpy, gc2); } #define IBEAM_WIDTH 2 static void draw_i_beam (Display *dpy, Drawable d, Pixel color, int x, int y, int height) { XGCValues gcv; GC gc; gcv.foreground = color; gcv.line_width = IBEAM_WIDTH; gc = XCreateGC (dpy, d, GCForeground | GCLineWidth, &gcv); XDrawLine (dpy, d, gc, x, y, x, y + height); /* Ceci n'est pas une pipe */ XFreeGC (dpy, gc); } static int draw_dialog_line (window_state *ws, Drawable d, dialog_line *line, int left, int right, int y, Bool clear_p) { int w = right - left; int h; int xpad = 0, ypad = 0; XGlyphInfo overall; line_align align = line->align; int oleft = left; int tleft = left; int oright = right; int clip_w = 0; int gutter = 0; XRectangle rect; int xoff2 = 0; int yoff2 = 0; char *text2 = 0; char *text = line->text; int nlines = 1; /* Adjust left/right margins based on the type of the line. */ switch (line->type) { case LABEL: if (line->float_p && line->align == LEFT) { /* Add 1px to leave a little padding between the top border of the label and the ascenders. */ ypad = ws->shadow_width + 1; right = left + w/2 - ws->shadow_width * 2 - line->font->ascent / 2; align = RIGHT; } if (*line->text) text = text2 = xft_word_wrap (ws->dpy, line->font, line->text, right - left); break; case BUTTON: /* box is fixed width at 1/3, text centered */ align = CENTER; xpad = 0; /* make the buttons a little taller than everything else */ /* Add 1px as above */ ypad = ws->shadow_width + line->font->ascent / 2 + 1; gutter = ws->shadow_width; clear_p = True; switch (line->align) { case LEFT: right = left + w/3 - xpad; break; case CENTER: xpad = ws->shadow_width * 2; left += w/3 + xpad; right -= w/3 + xpad; break; case RIGHT: left = right - w/3 + xpad; break; } oright = right; xpad = 0; break; case TEXT: /* box is fixed width at 1/2, text left */ case TEXT_RO: align = LEFT; oleft = left + xoff2; clear_p = True; xpad = ws->shadow_width + line->font->ascent / 4; /* Add 1px as above */ ypad = ws->shadow_width + 1; gutter = ws->shadow_width; if (gutter < 2) gutter = 2; switch (line->align) { case LEFT: right = left + w/2; break; case RIGHT: left = right - w/2; break; case CENTER: abort(); break; } /* If the text is longer than the field, scroll horizontally to show the end of the text instead of the beginning. */ XftTextExtentsUtf8_multi (ws->dpy, line->font, (FcChar8 *) text, strlen(text), &overall); if (overall.width >= w/2 - ws->shadow_width * 2 - IBEAM_WIDTH) { align = RIGHT; left = right - w/2; } break; default: abort(); break; } /* Clear out the area we're about to overwrite. */ h = nlines * (line->font->ascent + line->font->descent) + ypad*2; if (clear_p) { GC gc; XGCValues gcv; gcv.foreground = line->bg; gc = XCreateGC (ws->dpy, d, GCForeground, &gcv); XFillRectangle (ws->dpy, d, gc, left, y, oright-left, h); XFreeGC (ws->dpy, gc); } /* Draw borders if necessary. */ switch (line->type) { case LABEL: break; case BUTTON: case TEXT: case TEXT_RO: { Bool in_p = (line->type != BUTTON); if (line->button) { line->button->rect.x = left; line->button->rect.y = y; line->button->rect.width = right-left; line->button->rect.height = h; in_p = line->button->down_p || line->button->disabled_p; } tleft = left; draw_shaded_rectangle (ws->dpy, d, left, y, right-left, h, ws->shadow_width, (in_p ? ws->shadow_bottom : ws->shadow_top), (in_p ? ws->shadow_top : ws->shadow_bottom)); clip_w = ws->shadow_width; } break; default: abort(); break; } /* Draw the text inside our box. */ nlines = XftTextExtentsUtf8_multi (ws->dpy, line->font, (FcChar8 *) text, strlen(text), &overall); w = overall.width - overall.x; switch (align) { case LEFT: left = left + xpad; break; case RIGHT: left = right - w - xpad; break; case CENTER: oleft = left; left = left + xpad + (right - left - w) / 2; if (left < oleft) left = oleft; break; default: abort(); break; } rect.x = MAX (oleft, MAX (left, tleft + clip_w)); rect.width = MIN (oright, right) - rect.x - clip_w; rect.y = y + ypad - overall.y + line->font->ascent; rect.height = overall.height; XftDrawSetClipRectangles (ws->xftdraw, 0, 0, &rect, 1); if (line->type == BUTTON && line->button && (line->button->down_p || line->button->disabled_p)) xoff2 = yoff2 = MIN (ws->shadow_width, line->font->ascent/2); XftDrawStringUtf8_multi (ws->xftdraw, &line->fg, line->font, left + xoff2, y + ypad + yoff2 + line->font->ascent, (FcChar8 *) text, strlen (text), (align == LEFT ? 1 : align == CENTER ? 0 : -1)); # ifdef DEBUG_METRICS { GC gc; XGCValues gcv; int yy = y + ypad + yoff2 + line->font->ascent; gcv.foreground = line->fg.pixel; gc = XCreateGC (ws->dpy, d, GCForeground, &gcv); /* draw a line on the baseline of the text */ XDrawLine (ws->dpy, d, gc, 0, yy, right, yy); yy -= line->font->ascent; /* a line above the ascenders */ XDrawLine (ws->dpy, d, gc, left, yy, right, yy); yy += line->font->ascent + line->font->descent; /* and below the descenders */ XDrawLine (ws->dpy, d, gc, left, yy, right, yy); XFreeGC (ws->dpy, gc); } # endif if (line->i_beam) draw_i_beam (ws->dpy, d, ws->foreground, left + xoff2 + overall.width, y + ypad + yoff2, line->font->ascent + line->font->descent); XftDrawSetClip (ws->xftdraw, 0); if (text2) free (text2); y += ypad*2 + (nlines * (line->font->ascent + line->font->descent)) + gutter; return y; } static int draw_dialog_lines (window_state *sp, Drawable d, dialog_line *lines, int left, int right, int top) { int i; int maxy = 0; for (i = 0; lines[i].text; i++) { Bool clear_p = (i > 0 && lines[i-1].float_p ? False : True); int y = draw_dialog_line (sp, d, &lines[i], left, right, top, clear_p); if (y > maxy) maxy = y; if (! lines[i].float_p) top = maxy; } return top; } static pid_t fork_and_exec (Display *dpy, int argc, char **argv) { char buf [255]; pid_t forked = fork(); switch ((int) forked) { case -1: sprintf (buf, "%s: couldn't fork", blurb()); perror (buf); break; case 0: close (ConnectionNumber (dpy)); /* close display fd */ execvp (argv[0], argv); /* shouldn't return. */ sprintf (buf, "%s: pid %lu: couldn't exec %s", blurb(), (unsigned long) getpid(), argv[0]); perror (buf); exit (1); /* exits child fork */ break; default: /* parent fork */ if (verbose_p) { int i; fprintf (stderr, "%s: pid %lu: launched", blurb(), (unsigned long) forked); for (i = 0; i < argc; i++) fprintf (stderr, " %s", argv[i]); fprintf (stderr, "\n"); } break; } return forked; } /* Loading resources */ static void resource_keys (window_state *ws, const char **name, const char **rclass) { const char *theme = ws->dialog_theme; const char *name2 = (ws->splash_p ? "splash" : "passwd"); const char *class2 = "Dialog"; static char res[200], rclass2[200]; char *s; /* First try $THEME."Dialog.value" */ sprintf (res, "%s.%s.%s", theme, name2, *name); sprintf (rclass2, "%s.%s.%s", theme, class2, *rclass); s = get_string_resource (ws->dpy, res, rclass2); if (s && *s) goto DONE; /* Next try "default.Dialog.value" */ if (s) free (s); theme = "default"; sprintf (res, "%s.%s.%s", theme, name2, *name); sprintf (rclass2, "%s.%s.%s", theme, class2, *rclass); s = get_string_resource (ws->dpy, res, rclass2); if (s && *s) goto DONE; /* Next try "Dialog.value" */ if (s) free (s); sprintf (res, "%s.%s", theme, *name); sprintf (rclass2, "%s.%s", theme, *rclass); s = get_string_resource (ws->dpy, res, rclass2); if (s && *s) goto DONE; DONE: *name = res; *rclass = rclass2; if (s) free (s); } static char * get_str (window_state *ws, const char *name, const char *rclass) { resource_keys (ws, &name, &rclass); return get_string_resource (ws->dpy, (char *) name, (char *) rclass); } static XftFont * get_font (window_state *ws, const char *name) { const char *rclass = "Font"; XftFont *f; char *s; resource_keys (ws, &name, &rclass); s = get_string_resource (ws->dpy, (char *) name, (char *) rclass); if (!s || !*s) s = "sans-serif 14"; f = load_xft_font_retry (ws->dpy, DefaultScreen(ws->dpy), s); if (!f) abort(); return f; } static unsigned long get_color (window_state *ws, const char *name, const char *rclass) { resource_keys (ws, &name, &rclass); return get_pixel_resource (ws->dpy, DefaultColormapOfScreen (ws->screen), (char *) name, (char *) rclass); } static void get_xft_color (window_state *ws, XftColor *ret, const char *name, const char *rclass) { char *s; resource_keys (ws, &name, &rclass); s = get_string_resource (ws->dpy, (char *) name, (char *) rclass); if (!s || !*s) s = "black"; XftColorAllocName (ws->dpy, DefaultVisualOfScreen(ws->screen), DefaultColormapOfScreen (ws->screen), s, ret); } static int get_int (window_state *ws, const char *name, const char *rclass) { resource_keys (ws, &name, &rclass); return get_integer_resource (ws->dpy, (char *) name, (char *) rclass); } /* Decide where on the X11 screen to place the dialog. This is complicated because, in the face of RANDR and Xinerama, we want to center it on a *monitor*, not on what X calls a 'Screen'. So get the monitor state, then figure out which one of those the mouse is in. */ static void splash_pick_window_position (Display *dpy, Position *xP, Position *yP) { Window pointer_root, pointer_child; int root_x = 0, root_y = 0, win_x, win_y; unsigned int mask; monitor **monitors; monitor *m = 0; int i; XQueryPointer (dpy, RootWindow (dpy, 0), &pointer_root, &pointer_child, &root_x, &root_y, &win_x, &win_y, &mask); monitors = scan_monitors (dpy); if (!monitors || !*monitors) abort(); for (i = 0; monitors[i]; i++) { monitor *m0 = monitors[i]; if (m0->sanity == S_SANE && root_x >= m0->x && root_y >= m0->y && root_x < m0->x + m0->width && root_y < m0->y + m0->height) { m = m0; break; } } if (!m) { if (verbose_p) fprintf (stderr, "%s: mouse is not on any monitor?\n", blurb()); m = monitors[0]; } else if (verbose_p) fprintf (stderr, "%s: mouse is at %d,%d on monitor %d %dx%d+%d+%d \"%s\"\n", blurb(), root_x, root_y, m->id, m->width, m->height, m->x, m->y, (m->desc ? m->desc : "")); *xP = m->x + m->width/2; *yP = m->y + m->height/2; free_monitors (monitors); } static void unlock_cb (window_state *ws); /* This program only needs one option from the init file, so it just reads the .ad file and the .xscreensaver file directly rather than going through Xt and Xrm. */ static void init_line_handler (int lineno, const char *key, const char *val, void *closure) { window_state *ws = (window_state *) closure; if (val && *val && !strcmp (key, "dialogTheme")) { if (ws->dialog_theme) free (ws->dialog_theme); ws->dialog_theme = strdup (val); } } static void read_init_file_simple (window_state *ws) { const char *home = getenv("HOME"); const char *fn1 = AD_DIR "/XScreenSaver"; char *fn2; if (!home || !*home) return; fn2 = (char *) malloc (strlen(home) + 40); sprintf (fn2, "%s/.xscreensaver", home); if (debug_p) fprintf (stderr, "%s: reading %s\n", blurb(), fn1); parse_init_file (fn1, init_line_handler, ws); if (debug_p) fprintf (stderr, "%s: reading %s\n", blurb(), fn2); parse_init_file (fn2, init_line_handler, ws); if (verbose_p) fprintf (stderr, "%s: theme: %s\n", blurb(), (ws->dialog_theme ? ws->dialog_theme : "none")); } static void grab_keyboard_and_mouse (window_state *ws) { /* If we have been launched by xscreensaver, these grabs won't succeed, and that is expected. But if we are being run manually for debugging, they are necessary to avoid having events seen by two apps at once. (We don't bother to ungrab, that happens when we exit.) */ Display *dpy = ws->dpy; Window root = RootWindowOfScreen (ws->screen); XGrabKeyboard (dpy, root, True, GrabModeAsync, GrabModeAsync, CurrentTime); XGrabPointer (dpy, root, True, (ButtonPressMask | ButtonReleaseMask | EnterWindowMask | LeaveWindowMask | PointerMotionMask | PointerMotionHintMask | Button1MotionMask | Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask | ButtonMotionMask), GrabModeAsync, GrabModeAsync, root, None, CurrentTime); } static void get_keyboard_layout (window_state *ws) { # ifdef HAVE_XKB XkbStateRec state; XkbDescPtr desc = 0; Atom name = 0; char *namestr = 0; if (! ws->xkb_opcode) { if (! XkbQueryExtension (ws->dpy, 0, &ws->xkb_opcode, 0, 0, 0)) { ws->xkb_opcode = -1; /* Only try once */ if (verbose_p) fprintf (stderr, "%s: XkbQueryExtension failed\n", blurb()); return; } if (! XkbSelectEvents (ws->dpy, XkbUseCoreKbd, XkbMapNotifyMask | XkbStateNotifyMask, XkbMapNotifyMask | XkbStateNotifyMask)) { if (verbose_p) fprintf (stderr, "%s: XkbSelectEvents failed\n", blurb()); } } if (XkbGetState (ws->dpy, XkbUseCoreKbd, &state)) { if (verbose_p) fprintf (stderr, "%s: XkbGetState failed\n", blurb()); return; } desc = XkbGetKeyboard (ws->dpy, XkbAllComponentsMask, XkbUseCoreKbd); if (!desc || !desc->names) { if (verbose_p) fprintf (stderr, "%s: XkbGetKeyboard failed\n", blurb()); goto DONE; } name = desc->names->groups[state.group]; namestr = (name ? XGetAtomName (ws->dpy, name) : 0); if (!namestr) { if (verbose_p) fprintf (stderr, "%s: XkbGetKeyboard returned null layout\n", blurb()); goto DONE; } if (ws->kbd_layout_label) free (ws->kbd_layout_label); ws->kbd_layout_label = namestr; if (verbose_p) fprintf (stderr, "%s: kbd layout: %s\n", blurb(), namestr ? namestr : "null"); DONE: if (desc) XFree (desc); # endif /* HAVE_XKB */ } static double double_time (void) { struct timeval now; # ifdef GETTIMEOFDAY_TWO_ARGS struct timezone tzp; gettimeofday(&now, &tzp); # else gettimeofday(&now); # endif return (now.tv_sec + ((double) now.tv_usec * 0.000001)); } static void create_window (window_state *ws, int w, int h) { XSetWindowAttributes attrs; unsigned long attrmask; Window ow = ws->window; attrmask = CWOverrideRedirect | CWEventMask; attrs.override_redirect = True; attrs.event_mask = ExposureMask | VisibilityChangeMask; ws->window = XCreateWindow (ws->dpy, RootWindowOfScreen(ws->screen), ws->x, ws->y, w, h, 0, DefaultDepthOfScreen (ws->screen), InputOutput, DefaultVisualOfScreen(ws->screen), attrmask, &attrs); XSetWindowBackground (ws->dpy, ws->window, ws->background); XSetWindowColormap (ws->dpy, ws->window, ws->cmap); xscreensaver_set_wm_atoms (ws->dpy, ws->window, w, h, 0); if (ow) { XMapRaised (ws->dpy, ws->window); XDestroyWindow (ws->dpy, ow); } } /* Loads resources and creates and returns the global window state. */ static window_state * window_init (Widget root_widget, Bool splash_p) { Display *dpy = XtDisplay (root_widget); Screen *screen = XtScreen (root_widget); window_state *ws; Bool resource_error_p = False; ws = (window_state *) calloc (1, sizeof(*ws)); if (!ws) abort(); ws->splash_p = splash_p; ws->dpy = dpy; ws->screen = screen; ws->app = XtWidgetToApplicationContext (root_widget); /* Read default theme from resources before the init file. */ ws->dialog_theme = get_string_resource (ws->dpy, "dialogTheme", "DialogTheme"); if (!ws->dialog_theme || !*ws->dialog_theme) ws->dialog_theme = strdup ("default"); /* Read theme from init file before any other resources. */ read_init_file_simple (ws); { struct passwd *p = getpwuid (getuid()); if (!p || !p->pw_name || !*p->pw_name) abort(); ws->user = p->pw_name; } ws->cmap = XCreateColormap (dpy, RootWindowOfScreen (screen), /* Old skool */ DefaultVisualOfScreen (screen), AllocNone); ws->newlogin_cmd = get_str (ws, "newLoginCommand", "NewLoginCommand"); ws->date_format = get_str (ws, "dateFormat", "DateFormat"); ws->show_stars_p = get_boolean_resource (ws->dpy, "passwd.asterisks", "Passwd.Boolean"); /* Put the version number in the label. */ { char *version = strdup (screensaver_id + 17); char *year = strchr (version, '-'); char *s = strchr (version, ' '); *s = 0; year = strchr (year+1, '-') + 1; s = strchr (year, ')'); *s = 0; ws->heading_label = (char *) malloc (100); ws->version = strdup(version); sprintf (ws->heading_label, "XScreenSaver %.4s, v%.10s", year, version); if (splash_p) { ws->body_label = (char *) malloc (100); sprintf (ws->body_label, _("Copyright \xC2\xA9 1991-%.4s by\nJamie Zawinski "), year); } } ws->heading_font = get_font (ws, "headingFont"); ws->button_font = get_font (ws, "buttonFont"); ws->body_font = get_font (ws, "bodyFont"); ws->error_font = get_font (ws, "errorFont"); ws->label_font = get_font (ws, "labelFont"); ws->date_font = get_font (ws, "dateFont"); ws->hostname_font = get_font (ws, "unameFont"); ws->foreground = get_color (ws, "foreground", "Foreground"); ws->background = get_color (ws, "background", "Background"); get_xft_color (ws, &ws->xft_foreground, "foreground", "Foreground"); get_xft_color (ws, &ws->xft_text_foreground, "text.foreground", "Text.Foreground"); get_xft_color (ws, &ws->xft_error_foreground, "error.foreground", "Error.Foreground"); get_xft_color (ws, &ws->xft_button_foreground, "button.foreground", "Button.Foreground"); ws->shadow_top = get_color (ws, "topShadowColor", "Foreground"); ws->shadow_bottom = get_color (ws, "bottomShadowColor", "Background"); ws->border_color = get_color (ws, "borderColor", "BorderColor"); ws->passwd_background = get_color (ws, "text.background", "Text.Background"); ws->button_background = get_color (ws, "button.background", "Button.Background"); ws->thermo_foreground = get_color (ws, "thermometer.foreground", "Thermometer.Foreground"); ws->thermo_background = get_color ( ws, "thermometer.background", "Thermometer.Background"); ws->logo_background = get_color ( ws, "logo.background", "Logo.Background"); if (resource_error_p) { /* Make sure the error messages show up. */ ws->foreground = BlackPixelOfScreen (screen); ws->background = WhitePixelOfScreen (screen); } ws->preferred_logo_width = get_int (ws, "logo.width", "Logo.Width"); ws->preferred_logo_height = get_int (ws, "logo.height", "Logo.Height"); ws->thermo_width = get_int (ws, "thermometer.width", "Thermometer.Width"); ws->shadow_width = get_int (ws, "shadowWidth", "ShadowWidth"); ws->border_width = get_int (ws, "borderWidth", "BorderWidth"); ws->internal_padding = get_int (ws, "internalPadding", "InternalPadding"); if (ws->preferred_logo_width == 0) ws->preferred_logo_width = 150; if (ws->preferred_logo_height == 0) ws->preferred_logo_height = 150; if (ws->internal_padding == 0) ws->internal_padding = 15; if (ws->thermo_width == 0) ws->thermo_width = ws->shadow_width; if (ws->splash_p) ws->thermo_width = 0; # ifdef HAVE_UNAME if (!splash_p && get_boolean_resource (ws->dpy, "passwd.uname", "Passwd.Boolean")) { struct utsname uts; if (!uname (&uts) && *uts.nodename) ws->hostname_label = strdup (uts.nodename); } # endif get_keyboard_layout (ws); /* Load the logo pixmap, based on font size */ { int x, y; unsigned int bw, d; Window root = RootWindowOfScreen(ws->screen); Visual *visual = DefaultVisualOfScreen (ws->screen); int logo_size = (ws->heading_font->ascent > 24 ? 2 : 1); ws->logo_pixmap = xscreensaver_logo (screen, visual, root, ws->cmap, ws->background, &ws->logo_pixels, &ws->logo_npixels, &ws->logo_clipmask, logo_size); if (!ws->logo_pixmap) abort(); XGetGeometry (dpy, ws->logo_pixmap, &root, &x, &y, &ws->logo_width, &ws->logo_height, &bw, &d); } splash_pick_window_position (ws->dpy, &ws->cx, &ws->cy); ws->x = ws->y = 0; create_window (ws, 1, 1); /* Select SubstructureNotifyMask on the root window so that we know when another process has mapped a window, so that we can make our window always be on top. */ { Window root = RootWindowOfScreen (ws->screen); XWindowAttributes xgwa; XGetWindowAttributes (ws->dpy, root, &xgwa); XSelectInput (ws->dpy, root, xgwa.your_event_mask | SubstructureNotifyMask); } ws->newlogin_button_state.cmd = ws->newlogin_cmd; ws->demo_button_state.cmd = get_string_resource (ws->dpy, "demoCommand", "Command"); { char *load = get_string_resource (ws->dpy, "loadURL", "Command"); char *url = get_string_resource (ws->dpy, "helpURL", "URL"); if (load && *load && url && *url) { char *cmd = (char *) malloc (strlen(load) + (strlen(url) * 5) + 10); sprintf (cmd, load, url, url, url, url, url); ws->help_button_state.cmd = cmd; } } ws->unlock_button_state.fn = unlock_cb; grab_keyboard_and_mouse (ws); return ws; } #ifdef DEBUG_STACKING static void describe_window (Display *dpy, Window w) { XClassHint ch; char *name = 0; if (XGetClassHint (dpy, w, &ch)) { fprintf (stderr, "0x%lx \"%s\", \"%s\"\n", (unsigned long) w, ch.res_class, ch.res_name); XFree (ch.res_class); XFree (ch.res_name); } else if (XFetchName (dpy, w, &name) && name) { fprintf (stderr, "0x%lx \"%s\"\n", (unsigned long) w, name); XFree (name); } else { fprintf (stderr, "0x%lx (untitled)\n", (unsigned long) w); } } #endif /* DEBUG_STACKING */ /* Returns true if some other window is on top of this one. */ static Bool window_occluded_p (Display *dpy, Window window) { int screen; # ifdef DEBUG_STACKING fprintf (stderr, "\n"); # endif for (screen = 0; screen < ScreenCount (dpy); screen++) { int i; Window root = RootWindow (dpy, screen); Window root2 = 0, parent = 0, *kids = 0; unsigned int nkids = 0; Bool saw_our_window_p = False; Bool saw_later_window_p = False; if (! XQueryTree (dpy, root, &root2, &parent, &kids, &nkids)) { # ifdef DEBUG_STACKING fprintf (stderr, "%s: XQueryTree failed\n", blurb()); # endif continue; } for (i = 0; i < nkids; i++) { if (kids[i] == window) { saw_our_window_p = True; # ifdef DEBUG_STACKING fprintf (stderr, "%s: our window: ", blurb()); describe_window (dpy, kids[i]); # endif } else if (saw_our_window_p) { saw_later_window_p = True; # ifdef DEBUG_STACKING fprintf (stderr, "%s: higher window: ", blurb()); describe_window (dpy, kids[i]); # endif break; } else { # ifdef DEBUG_STACKING fprintf (stderr, "%s: lower window: ", blurb()); describe_window (dpy, kids[i]); # endif } } if (kids) XFree ((char *) kids); if (saw_later_window_p) return True; else if (saw_our_window_p) return False; /* else our window is not on this screen; keep going, try the next. */ } /* Window doesn't exist? */ # ifdef DEBUG_STACKING fprintf (stderr, "%s: our window isn't on the screen\n", blurb()); # endif return False; } /* Strip leading and trailing whitespace. */ static char * trim (const char *s) { char *s2; int L; if (!s) return 0; while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n') s++; s2 = strdup (s); L = strlen (s2); while (L > 0 && (s2[L-1] == ' ' || s2[L-1] == '\t' || s2[L-1] == '\r' || s2[L-1] == '\n')) s2[--L] = 0; return s2; } /* Repaint the entire window. */ static void window_draw (window_state *ws) { Display *dpy = ws->dpy; Screen *screen = DefaultScreenOfDisplay (dpy); Window root = RootWindowOfScreen (screen); Visual *visual = DefaultVisualOfScreen(screen); int depth = DefaultDepthOfScreen (screen); XWindowAttributes xgwa; # define MIN_COLUMNS 22 /* Set window width based on headingFont ascent. */ int ext_border = (ws->internal_padding / 2 + ws->shadow_width + ws->border_width); Pixmap dbuf; unsigned int logo_frame_width, logo_frame_height; unsigned int window_width, window_height; unsigned int text_left, text_right; unsigned int thermo_x; unsigned int x, y; GC gc; XGCValues gcv; char date_text[100]; time_t now = time ((time_t *) 0); struct tm *tm = localtime (&now); double ratio = 1 - ((double_time() - ws->start_time) / (ws->end_time - ws->start_time)); dialog_line *lines = (dialog_line *) calloc (ws->nmsgs + 40, sizeof(*lines)); Bool emitted_user_p = False; int i = 0, j; XGetWindowAttributes (ws->dpy, ws->window, &xgwa); if (!lines) abort(); strftime (date_text, sizeof(date_text)-2, ws->date_format, tm); logo_frame_width = (ws->logo_width + ws->internal_padding * 2 + ws->shadow_width * 2); logo_frame_height = logo_frame_width; if (logo_frame_width < ws->preferred_logo_width) logo_frame_width = ws->preferred_logo_width; if (logo_frame_height < ws->preferred_logo_height) logo_frame_height = ws->preferred_logo_height; thermo_x = ext_border * 1.5 + logo_frame_width + ws->shadow_width; text_left = (thermo_x + ws->internal_padding + (ws->thermo_width ? ws->thermo_width + ws->shadow_width * 3 : 0)); text_right = text_left + ws->heading_font->ascent * MIN_COLUMNS; window_width = text_right + ws->internal_padding + ext_border; window_height = window_width * 3; /* reduced later */ dbuf = XCreatePixmap (dpy, root, window_width, window_height, depth); gc = XCreateGC (dpy, dbuf, 0, &gcv); XSetForeground (dpy, gc, ws->background); XFillRectangle (dpy, dbuf, gc, 0, 0, window_width, window_height); if (ws->xftdraw) XftDrawDestroy (ws->xftdraw); ws->xftdraw = XftDrawCreate (dpy, dbuf, visual, xgwa.colormap); lines[i].text = ws->heading_label; /* XScreenSaver */ lines[i].font = ws->heading_font; lines[i].fg = ws->xft_foreground; lines[i].bg = ws->background; lines[i].type = LABEL; lines[i].align = CENTER; i++; if (time ((time_t *) 0) - XSCREENSAVER_RELEASED > 60*60*24*30*17) { lines[i].text = _("Update available!\nThis version is very old.\n"); lines[i].font = ws->error_font; lines[i].fg = ws->xft_error_foreground; lines[i].bg = ws->background; lines[i].type = LABEL; lines[i].align = CENTER; i++; } else if (strstr (ws->version, "a") || strstr (ws->version, "b")) { lines[i].text = _("PRE-RELEASE VERSION"); lines[i].font = ws->error_font; lines[i].fg = ws->xft_error_foreground; lines[i].bg = ws->background; lines[i].type = LABEL; lines[i].align = CENTER; i++; } if (ws->hostname_label && *ws->hostname_label) { lines[i].text = ws->hostname_label; lines[i].font = ws->hostname_font; lines[i].fg = ws->xft_foreground; lines[i].bg = ws->background; lines[i].type = LABEL; lines[i].align = CENTER; i++; } # define BLANK_LINE \ lines[i].text = ""; \ lines[i].font = ws->body_font; \ lines[i].fg = ws->xft_foreground; \ lines[i].bg = ws->background; \ lines[i].type = LABEL; \ lines[i].align = CENTER; \ i++ BLANK_LINE; if (debug_p && !ws->splash_p) { lines[i].text = _("DEBUG MODE:\nAll keystrokes are being logged to stderr.\n"); lines[i].font = ws->error_font; lines[i].fg = ws->xft_error_foreground; lines[i].bg = ws->background; lines[i].type = LABEL; lines[i].align = CENTER; i++; } if (ws->body_label && *ws->body_label) { lines[i].text = ws->body_label; /* Copyright or error message */ lines[i].font = ws->body_font; lines[i].fg = ws->xft_foreground; lines[i].bg = ws->background; lines[i].type = LABEL; lines[i].align = CENTER; i++; BLANK_LINE; } for (j = 0; j < ws->nmsgs; j++) /* PAM msgs */ { switch (ws->msgs[j].type) { case AUTH_MSGTYPE_INFO: case AUTH_MSGTYPE_ERROR: lines[i].text = trim (ws->msgs[j].msg); lines[i].font = (ws->msgs[j].type == AUTH_MSGTYPE_ERROR ? ws->error_font : ws->body_font); lines[i].fg = (ws->msgs[j].type == AUTH_MSGTYPE_ERROR ? ws->xft_error_foreground : ws->xft_foreground); lines[i].bg = ws->background; lines[i].type = LABEL; lines[i].align = CENTER; i++; break; case AUTH_MSGTYPE_PROMPT_NOECHO: case AUTH_MSGTYPE_PROMPT_ECHO: /* Show the logged in user before the first password field. */ if (!emitted_user_p) { lines[i].text = _("Username:"); lines[i].font = ws->label_font; lines[i].fg = ws->xft_foreground; lines[i].bg = ws->background; lines[i].type = LABEL; lines[i].align = LEFT; lines[i].float_p = True; i++; lines[i].text = ws->user; /* $USER */ lines[i].font = ws->label_font; lines[i].fg = ws->xft_text_foreground; lines[i].bg = ws->passwd_background; lines[i].type = TEXT_RO; lines[i].align = RIGHT; i++; } lines[i].text = trim (ws->msgs[j].msg); /* PAM prompt text */ lines[i].font = ws->label_font; lines[i].fg = ws->xft_foreground; lines[i].bg = ws->background; lines[i].type = LABEL; lines[i].align = LEFT; lines[i].float_p = True; i++; lines[i].text = (ws->auth_state == AUTH_FINISHED ? _("Checking...") : ws->msgs[j].type == AUTH_MSGTYPE_PROMPT_ECHO ? ws->plaintext_passwd /* Hopefully UTF-8 */ : ws->show_stars_p ? ws->censored_passwd : ""); lines[i].font = ws->label_font; lines[i].fg = ws->xft_text_foreground; lines[i].bg = ws->passwd_background; lines[i].type = TEXT; lines[i].align = RIGHT; lines[i].i_beam = (ws->i_beam && ws->auth_state != AUTH_FINISHED); i++; /* Show the current time below the first password field only. */ if (*date_text && !emitted_user_p) { lines[i].text = date_text; lines[i].font = ws->date_font; lines[i].fg = ws->xft_foreground; lines[i].bg = ws->background; lines[i].type = LABEL; lines[i].align = RIGHT; i++; } /* Show the current keyboard layout below that. */ if (ws->kbd_layout_label && *ws->kbd_layout_label && !emitted_user_p) { lines[i].text = ws->kbd_layout_label; lines[i].font = ws->date_font; lines[i].fg = ws->xft_foreground; lines[i].bg = ws->background; lines[i].type = LABEL; lines[i].align = RIGHT; i++; } emitted_user_p = True; break; default: abort(); break; } } lines[i].text = 0; y = draw_dialog_lines (ws, dbuf, lines, text_left, text_right, ws->border_width + ws->internal_padding + ws->shadow_width); window_height = y; window_height += (ws->button_font->ascent * 4); window_height += (ws->internal_padding + ws->shadow_width * 2 + ws->border_width); /* Keep logo area square or taller */ if (window_height < logo_frame_height + ws->shadow_width * 4) window_height = logo_frame_height + ws->shadow_width * 4; /* Fitt's Law: It is distracting to reduce the height of the window after creation. */ if (window_height < ws->min_height) window_height = ws->min_height; ws->min_height = window_height; /* Now do a second set of lines for the buttons at the bottom. */ memset (lines, 0, sizeof(*lines)); i = 0; if (ws->splash_p) { lines[i].text = _("Settings"); lines[i].font = ws->button_font; lines[i].fg = ws->xft_button_foreground; lines[i].bg = ws->button_background; lines[i].type = BUTTON; lines[i].align = LEFT; lines[i].float_p = True; lines[i].button = &ws->demo_button_state; i++; lines[i].text = _("Help"); lines[i].font = ws->button_font; lines[i].fg = ws->xft_button_foreground; lines[i].bg = ws->button_background; lines[i].type = BUTTON; lines[i].align = RIGHT; lines[i].button = &ws->help_button_state; i++; } else { if (ws->newlogin_cmd && *ws->newlogin_cmd) { lines[i].text = _("New Login"); lines[i].font = ws->button_font; lines[i].fg = ws->xft_button_foreground; lines[i].bg = ws->button_background; lines[i].type = BUTTON; lines[i].align = LEFT; lines[i].float_p = True; lines[i].button = &ws->newlogin_button_state; i++; } lines[i].text = _("OK"); lines[i].font = ws->button_font; lines[i].fg = ws->xft_button_foreground; lines[i].bg = ws->button_background; lines[i].type = BUTTON; lines[i].align = RIGHT; lines[i].button = &ws->unlock_button_state; i++; } lines[i].text = 0; y = draw_dialog_lines (ws, dbuf, lines, text_left, text_right, window_height - ws->internal_padding - ext_border - ws->shadow_width - (ws->button_font->ascent * 2)); /* The thermometer */ if (ws->thermo_width) { if (ws->auth_state != AUTH_NOTIFY) { int thermo_w = ws->thermo_width; int thermo_h = window_height - ext_border * 2; int thermo_h2 = thermo_h - ws->shadow_width * 2; int thermo_h3 = thermo_h2 * (1.0 - ratio); XSetForeground (dpy, gc, ws->thermo_foreground); XFillRectangle (dpy, dbuf, gc, thermo_x + ws->shadow_width, ext_border + ws->shadow_width, thermo_w, thermo_h2); if (thermo_h3 > 0) { XSetForeground (dpy, gc, ws->thermo_background); XFillRectangle (dpy, dbuf, gc, thermo_x + ws->shadow_width, ext_border + ws->shadow_width, thermo_w, thermo_h3); } } draw_shaded_rectangle (dpy, dbuf, thermo_x, ext_border, ws->thermo_width + ws->shadow_width * 2, window_height - ext_border * 2, ws->shadow_width, ws->shadow_bottom, ws->shadow_top); } /* The logo, centered vertically. */ { int bot = window_height - ext_border * 2; int xoff = (logo_frame_width - ws->logo_width) / 2; int yoff = (bot - ws->logo_height) / 2; x = ext_border; y = ext_border; XSetForeground (dpy, gc, ws->logo_background); XFillRectangle (dpy, dbuf, gc, x, y, logo_frame_width, bot); XSetForeground (dpy, gc, ws->foreground); XSetBackground (dpy, gc, ws->background); XSetClipMask (dpy, gc, ws->logo_clipmask); XSetClipOrigin (dpy, gc, x + xoff, y + yoff); XCopyArea (dpy, ws->logo_pixmap, dbuf, gc, 0, 0, ws->logo_width, ws->logo_height, x + xoff, y + yoff); XSetClipMask (dpy, gc, 0); draw_shaded_rectangle (dpy, dbuf, x, y, logo_frame_width, bot, ws->shadow_width, ws->shadow_bottom, ws->shadow_top); } /* The window's shadow */ draw_shaded_rectangle (dpy, dbuf, ws->border_width, ws->border_width, window_width - ws->border_width * 2, window_height - ws->border_width * 2, ws->shadow_width, ws->shadow_top, ws->shadow_bottom); /* The window's border */ draw_shaded_rectangle (dpy, dbuf, 0, 0, window_width, window_height, ws->border_width, ws->border_color, ws->border_color); /* Now that everything has been rendered into the pixmap, reshape the window and copy the pixmap to it. This double-buffering is to prevent flicker. You'd think we could just reshape the window and then XMapRaised, but no. With the XCompose extension enabled and the "Mutter" window manager, the dialog window was sometimes not appearing on the screen, despite the fact that XQueryTree reported it as the topmost window. The mouse pointer also reflected it being there, but it wasn't visible. This is probably related to the "XCompositeGetOverlayWindow" window in some way, which is a magic, invisible window that is secretly excluded from the list returned by XQueryTree, but I can't figure out what was really going on, except that XMapRaised did not make my OverrideRedirect window appear topmost on the screen. However! Though XMapRaised was not working, it turns out that destroying and re-creating the window *does* make it appear. So we do that, any time the window's shape has changed, or some other window has raised above it. Calling XQueryTree at 30fps could conceivably be a performance problem, if there are thousands of windows on the screen. But here we are. */ { Bool size_changed_p, occluded_p; /* It's distracting to move or shrink the window after creating it. */ if (xgwa.height > 100 && xgwa.height > window_height) window_height = xgwa.height; if (! ws->x) { ws->x = ws->cx - (window_width / 2); ws->y = ws->cy - (window_height / 2); } /* If there is any change to the window's size, or if the window is not on top, destroy and re-create the window. */ size_changed_p = !(xgwa.x == ws->x && xgwa.y == ws->y && xgwa.width == window_width && xgwa.height == window_height); occluded_p = (!size_changed_p && window_occluded_p (ws->dpy, ws->window)); if (size_changed_p || occluded_p) { # if 0 /* Window sometimes disappears under Mutter 3.30.2, Feb 2021. */ XWindowChanges wc; wc.x = ws->x; wc.y = ws->y; wc.width = window_width; wc.height = window_height; if (verbose_p) fprintf (stderr, "%s: reshaping window %dx%d+%d+%d\n", blurb(), wc.width, wc.height, wc.x, wc.y); XConfigureWindow (ws->dpy, ws->window, CWX|CWY|CWWidth|CWHeight, &wc); # else if (verbose_p) fprintf (stderr, "%s: re-creating window: %s\n", blurb(), size_changed_p ? "size changed" : "occluded"); create_window (ws, window_width, window_height); # endif XMapRaised (ws->dpy, ws->window); XInstallColormap (ws->dpy, ws->cmap); } } XFreeGC (dpy, gc); gc = XCreateGC (dpy, ws->window, 0, &gcv); XCopyArea (dpy, dbuf, ws->window, gc, 0, 0, window_width, window_height, 0, 0); XSync (dpy, False); XFreeGC (dpy, gc); XFreePixmap (dpy, dbuf); free (lines); if (verbose_p > 1) { static time_t last = 0; static int count = 0; count++; if (now > last) { double fps = count / (double) (now - last); fprintf (stderr, "%s: FPS: %0.1f\n", blurb(), fps); count = 0; last = now; } } } /* Unmaps the window and frees window_state. */ static void destroy_window (window_state *ws) { XEvent event; memset (ws->plaintext_passwd, 0, sizeof(ws->plaintext_passwd)); memset (ws->plaintext_passwd_char_size, 0, sizeof(ws->plaintext_passwd_char_size)); memset (ws->censored_passwd, 0, sizeof(ws->censored_passwd)); if (ws->timer) { XtRemoveTimeOut (ws->timer); ws->timer = 0; } if (ws->cursor_timer) { XtRemoveTimeOut (ws->cursor_timer); ws->cursor_timer = 0; } while (XCheckMaskEvent (ws->dpy, PointerMotionMask, &event)) if (verbose_p) fprintf (stderr, "%s: discarding MotionNotify event\n", blurb()); if (ws->window) { XDestroyWindow (ws->dpy, ws->window); ws->window = 0; } if (ws->heading_label) free (ws->heading_label); if (ws->date_format) free (ws->date_format); if (ws->hostname_label) free (ws->hostname_label); if (ws->kbd_layout_label) free (ws->kbd_layout_label); if (ws->heading_font) XftFontClose (ws->dpy, ws->heading_font); if (ws->body_font) XftFontClose (ws->dpy, ws->body_font); if (ws->label_font) XftFontClose (ws->dpy, ws->label_font); if (ws->date_font) XftFontClose (ws->dpy, ws->date_font); if (ws->button_font) XftFontClose (ws->dpy, ws->button_font); if (ws->hostname_font) XftFontClose (ws->dpy, ws->hostname_font); XftColorFree (ws->dpy, DefaultVisualOfScreen (ws->screen), DefaultColormapOfScreen (ws->screen), &ws->xft_foreground); XftColorFree (ws->dpy, DefaultVisualOfScreen (ws->screen), DefaultColormapOfScreen (ws->screen), &ws->xft_button_foreground); XftColorFree (ws->dpy, DefaultVisualOfScreen (ws->screen), DefaultColormapOfScreen (ws->screen), &ws->xft_text_foreground); XftColorFree (ws->dpy, DefaultVisualOfScreen (ws->screen), DefaultColormapOfScreen (ws->screen), &ws->xft_error_foreground); XftDrawDestroy (ws->xftdraw); # if 0 /* screw this, we're exiting anyway */ if (ws->foreground != black && ws->foreground != white) XFreeColors (ws->dpy, ws->cmap, &ws->foreground, 1, 0L); if (ws->background != black && ws->background != white) XFreeColors (ws->dpy, ws->cmap, &ws->background, 1, 0L); if (ws->button_background != black && ws->button_background != white) XFreeColors (ws->dpy, ws->cmap, &ws->button_background, 1, 0L); if (ws->passwd_background != black && ws->passwd_background != white) XFreeColors (ws->dpy, ws->cmap, &ws->passwd_background, 1, 0L); if (ws->thermo_foreground != black && ws->thermo_foreground != white) XFreeColors (ws->dpy, ws->cmap, &ws->thermo_foreground, 1, 0L); if (ws->thermo_background != black && ws->thermo_background != white) XFreeColors (ws->dpy, ws->cmap, &ws->thermo_background, 1, 0L); if (ws->logo_background != black && ws->logo_background != white) XFreeColors (ws->dpy, ws->cmap, &ws->logo_background, 1, 0L); if (ws->shadow_top != black && ws->shadow_top != white) XFreeColors (ws->dpy, ws->cmap, &ws->shadow_top, 1, 0L); if (ws->shadow_bottom != black && ws->shadow_bottom != white) XFreeColors (ws->dpy, ws->cmap, &ws->shadow_bottom, 1, 0L); # endif if (ws->logo_pixmap) XFreePixmap (ws->dpy, ws->logo_pixmap); if (ws-> logo_clipmask) XFreePixmap (ws->dpy, ws->logo_clipmask); if (ws->logo_pixels) { if (ws->logo_npixels) XFreeColors (ws->dpy, ws->cmap, ws->logo_pixels, ws->logo_npixels, 0L); free (ws->logo_pixels); ws->logo_pixels = 0; ws->logo_npixels = 0; } XSync (ws->dpy, False); memset (ws, 0, sizeof(*ws)); free (ws); } static void unlock_cb (window_state *ws) { if (ws->auth_state == AUTH_READ) ws->auth_state = AUTH_FINISHED; } /* We store the count and last time of authorization failures on a property on the root window, so that on subsequent runs of this program that succeed, we can warn the user that someone tried to log in and failed. */ static void persistent_auth_status_failure (window_state *ws, Bool increment_p, Bool clear_p, int *count_ret, time_t *time_ret) { Display *dpy = ws->dpy; Window w = RootWindow (dpy, 0); /* always screen 0 */ Atom prop = XInternAtom (ws->dpy, LOCK_FAILURE_ATOM, False); int count = 0; time_t tt = 0; Atom type; unsigned char *dataP = 0; int format; unsigned long nitems, bytesafter; if (increment_p && clear_p) abort(); /* Read the old property so that we can increment it. */ if (XGetWindowProperty (dpy, w, prop, 0, 999, False, XA_INTEGER, &type, &format, &nitems, &bytesafter, &dataP) == Success && type == XA_INTEGER && nitems >= 2 && dataP) { count = ((PROP32 *) dataP) [0]; tt = ((PROP32 *) dataP) [1]; /* Y2038 bug: unsigned 32 bit time_t */ if (verbose_p) fprintf (stderr, "%s: previous auth failures: %d @ %lu\n", blurb(), count, (unsigned long) tt); } if (dataP) XFree (dataP); if (clear_p) { XDeleteProperty (dpy, w, prop); if (verbose_p) fprintf (stderr, "%s: deleted auth failure property\n", blurb()); } else if (increment_p) { PROP32 vv[2]; count++; /* Remember the time of the *oldest* failed login. A failed login 5 seconds ago does not mean we should skip warning about a failed login yesterday. */ if (tt <= 0) tt = time ((time_t *) 0); vv[0] = (PROP32) count; vv[1] = (PROP32) tt; XChangeProperty (dpy, w, prop, XA_INTEGER, 32, PropModeReplace, (unsigned char *) vv, 2); if (verbose_p) fprintf (stderr, "%s: saved auth failure: %d @ %lu\n", blurb(), count, (unsigned long) tt); } if (count_ret) *count_ret = count; if (time_ret) *time_ret = tt; } static void handle_keypress (window_state *ws, XKeyEvent *event) { unsigned char decoded [MAX_BYTES_PER_CHAR * 10]; /* leave some slack */ KeySym keysym = 0; /* XLookupString may return more than one character via XRebindKeysym; and on some systems it returns multi-byte UTF-8 characters (contrary to its documentation, which says it returns only Latin1.) It seems to only do so, however, if setlocale() has been called. See the code inside ENABLE_NLS in xscreensaver-auth.c. The X Keyboard Extension X11R6.4 documentation says: "When Xkb is present, XLookupString is allowed, but not required, to return strings in character sets other than ISO Latin-1, depending on the current locale." So I guess that means that multi-byte strings returned by XLookupString might not be UTF-8, and thus might not be compatible with XftDrawStringUtf8. */ int decoded_size = XLookupString (event, (char *)decoded, sizeof(decoded), &keysym, &ws->compose_status); if (decoded_size > MAX_BYTES_PER_CHAR) { /* The multi-byte character returned is too large. */ XBell (ws->dpy, 0); return; } decoded[decoded_size] = 0; /* Add 10% to the time remaining every time a key is pressed, but don't go past the max duration. */ { time_t now = time ((time_t *) 0); int max = get_seconds_resource (ws->dpy, "passwdTimeout", "Time"); int remain = ws->end_time - now; remain *= 1.1; if (remain > max) remain = max; if (remain < 3) remain = 3; ws->end_time = now + remain; } if (decoded_size == 1) /* Handle single-char commands */ { switch (*decoded) { case '\010': case '\177': /* Backspace */ { /* kludgey way to get the number of "logical" characters. */ int nchars = strlen (ws->plaintext_passwd_char_size); int nbytes = strlen (ws->plaintext_passwd); if (nbytes <= 0) XBell (ws->dpy, 0); else { int i; for (i = ws->plaintext_passwd_char_size[nchars-1]; i >= 0; i--) { if (nbytes < 0) abort(); ws->plaintext_passwd[nbytes--] = 0; } ws->plaintext_passwd_char_size[nchars-1] = 0; } } break; case '\012': case '\015': /* Enter */ unlock_cb (ws); break; case '\033': /* Escape */ ws->auth_state = AUTH_CANCEL; break; case '\025': case '\030': /* Erase line */ memset (ws->plaintext_passwd, 0, sizeof (ws->plaintext_passwd)); memset (ws->plaintext_passwd_char_size, 0, sizeof (ws->plaintext_passwd_char_size)); break; default: if (*decoded < ' ' && *decoded != '\t') /* Other ctrl char */ XBell (ws->dpy, 0); else goto SELF_INSERT; break; } } else { int nbytes, nchars; SELF_INSERT: nbytes = strlen (ws->plaintext_passwd); nchars = strlen (ws->plaintext_passwd_char_size); if (nchars + 1 >= sizeof (ws->plaintext_passwd_char_size)-1 || nbytes + decoded_size >= sizeof (ws->plaintext_passwd)-1) XBell (ws->dpy, 0); /* overflow */ else if (decoded_size == 0) ; /* Non-inserting keysym (Shift, Ctrl) */ else { ws->plaintext_passwd_char_size[nchars] = decoded_size; ws->plaintext_passwd_char_size[nchars+1] = 0; memcpy (ws->plaintext_passwd + nbytes, decoded, decoded_size); ws->plaintext_passwd[nbytes + decoded_size] = 0; } } /* Construct the string of asterisks. */ { char *out = ws->censored_passwd; int i; *out = 0; for (i = 0; i < MAX_PASSWD_CHARS && ws->plaintext_passwd_char_size[i]; i++) { const char *b = /* "\xE2\x80\xA2"; */ /* U+2022 Bullet */ "\xe2\x97\x8f"; /* U+25CF Black Circle */ strcat (out, b); out += strlen(out); } } } static Bool handle_button (window_state *ws, XEvent *xev, line_button_state *bs) { Bool mouse_in_box = (xev->xbutton.x_root >= (ws->x + bs->rect.x) && xev->xbutton.x_root < (ws->x + bs->rect.x + bs->rect.width) && xev->xbutton.y_root >= (ws->y + bs->rect.y) && xev->xbutton.y_root < (ws->y + bs->rect.y + bs->rect.height)); bs->down_p = (!bs->disabled_p && mouse_in_box && xev->xany.type != ButtonRelease); if (xev->xany.type == ButtonRelease && mouse_in_box && !bs->disabled_p) { bs->disabled_p = True; /* Only allow them to press the button once. */ if (bs->fn) bs->fn (ws); else if (bs->cmd) { int ac = 0; char *av[10]; av[ac++] = "/bin/sh"; av[ac++] = "-c"; av[ac++] = bs->cmd; av[ac] = 0; fork_and_exec (ws->dpy, ac, av); } else XBell (ws->dpy, 0); } return mouse_in_box; } static Bool handle_event (window_state *ws, XEvent *xev) { Bool refresh_p = False; switch (xev->xany.type) { case KeyPress: if (ws->splash_p) ws->auth_state = AUTH_CANCEL; else { handle_keypress (ws, &xev->xkey); ws->caps_p = (xev->xkey.state & LockMask); if (ws->auth_state == AUTH_NOTIFY) ws->auth_state = AUTH_CANCEL; } refresh_p = True; break; case ButtonPress: case ButtonRelease: { if (! (handle_button (ws, xev, &ws->newlogin_button_state) || handle_button (ws, xev, &ws->unlock_button_state) || handle_button (ws, xev, &ws->demo_button_state) || handle_button (ws, xev, &ws->help_button_state))) if (ws->splash_p && xev->xany.type == ButtonRelease) ws->auth_state = AUTH_CANCEL; refresh_p = True; } default: break; } return refresh_p; } /* Blink the I-beam cursor. */ static void cursor_timer (XtPointer closure, XtIntervalId *id) { window_state *ws = (window_state *) closure; int timeout = 0.7 * 1000 * (ws->i_beam ? 0.25 : 0.75); if (ws->cursor_timer) XtRemoveTimeOut (ws->cursor_timer); ws->cursor_timer = XtAppAddTimeOut (ws->app, timeout, cursor_timer, (XtPointer) ws); ws->i_beam = !ws->i_beam; } /* Redraw the window for the thermometer, and exit if the time has expired. */ static void thermo_timer (XtPointer closure, XtIntervalId *id) { window_state *ws = (window_state *) closure; int timeout = 1000/30; /* FPS */ double now = double_time(); if (now >= ws->end_time) ws->auth_state = AUTH_TIME; if (ws->timer) XtRemoveTimeOut (ws->timer); ws->timer = XtAppAddTimeOut (ws->app, timeout, thermo_timer, (XtPointer) ws); } static void gui_main_loop (window_state *ws, Bool splash_p, Bool notification_p) { int timeout; Bool refresh_p = True; if (splash_p) { timeout = get_seconds_resource (ws->dpy, "splashDuration", "Time"); if (timeout <= 1) timeout = 1; } else if (ws->auth_state == AUTH_NOTIFY) timeout = 5; else { timeout = get_seconds_resource (ws->dpy, "passwdTimeout", "Time"); if (timeout <= 5) timeout = 5; cursor_timer (ws, 0); } ws->start_time = double_time(); ws->end_time = ws->start_time + timeout; /* Since the "xscreensaver" process holds the mouse and keyboard grabbed while "xscreensaver-auth" is running, we don't receive normal KeyPress events. That means that the XInput2 extension is required in order to snoop on the keyboard in a way that bypasses grabs. */ if (! ws->xi_opcode) { Bool ov = verbose_p; verbose_p = False; init_xinput (ws->dpy, &ws->xi_opcode); verbose_p = ov; } thermo_timer (ws, 0); window_draw (ws); while (ws->auth_state == AUTH_READ || ws->auth_state == AUTH_NOTIFY) { XEvent xev; XtInputMask m = XtAppPending (ws->app); if (m & XtIMXEvent) /* Process timers then block on an X event (which we know is there) */ XtAppNextEvent (ws->app, &xev); else { if (m) /* Process timers only, don't block */ XtAppProcessEvent (ws->app, m); else { if (refresh_p) { /* Redraw when outstanding events have been processed. */ window_draw (ws); refresh_p = False; } /* No way to say "block until timer *or* X pending". Without this, the timer that changes auth_state will fire but then we will still be blocked until the next X event. */ usleep (1000000/30); } continue; } if ((m & ~XtIMXEvent) && !ws->splash_p) refresh_p = True; /* In auth mode, all timers refresh */ if (verbose_p || debug_p) print_xinput_event (ws->dpy, &xev, ""); /* Convert XInput events to Xlib events, for simplicity and familiarity. */ if (xev.xcookie.type == GenericEvent && xev.xcookie.extension == ws->xi_opcode && (xev.xcookie.data || XGetEventData (ws->dpy, &xev.xcookie))) { XEvent ev2; Bool ok = xinput_event_to_xlib (xev.xcookie.evtype, xev.xcookie.data, &ev2); XFreeEventData (ws->dpy, &xev.xcookie); if (ok) xev = ev2; } if (handle_event (ws, &xev)) refresh_p = True; XtDispatchEvent (&xev); switch (xev.xany.type) { /* I don't think we ever receive these, but if we do, redraw. */ case Expose: case GraphicsExpose: refresh_p = True; break; /* Likewise, receiving this event would be ideal, but we don't. */ case VisibilityNotify: refresh_p = True; if (verbose_p > 1) fprintf (stderr, "%s: VisibilityNotify\n", blurb()); break; /* When another override-redirect window is raised above us, we receive several ConfigureNotify events on the root window. */ case ConfigureNotify: if (verbose_p > 1) fprintf (stderr, "%s: ConfigureNotify\n", blurb()); break; case MappingNotify: /* This event is supposed to be sent when the keymap is changed. You would think that typing XK_ISO_Next_Group to change the keyboard layout would count as such. It does not. */ if (verbose_p) fprintf (stderr, "%s: MappingNotify\n", blurb()); get_keyboard_layout (ws); refresh_p = True; break; default: break; } /* Since MappingNotify doesn't work, we have to do this crap instead. */ if (xev.xany.type == ws->xkb_opcode) { XkbEvent *xkb = (XkbEvent *) &xev; if (verbose_p) fprintf (stderr, "%s: XKB event %d\n", blurb(), xkb->any.xkb_type); get_keyboard_layout (ws); refresh_p = True; } } /* Re-raw the window one last time, since it might sit here for a while while PAM does it's thing. */ window_draw (ws); XSync (ws->dpy, False); if (verbose_p) { const char *kind = (splash_p ? "splash" : notification_p ? "notification" : "authentication"); switch (ws->auth_state) { case AUTH_FINISHED: fprintf (stderr, "%s: %s input finished\n", blurb(), kind); break; case AUTH_CANCEL: fprintf (stderr, "%s: %s canceled\n", blurb(), kind); break; case AUTH_TIME: fprintf (stderr, "%s: %s timed out\n", blurb(), kind); break; default: break; } } } /* Pops up a dialog and waits for the user to complete it. Returns 0 on successful completion. Updates 'resp' with any entered response. */ static Bool dialog_session (window_state *ws, int nmsgs, const auth_message *msgs, auth_response *resp) { int i; ws->auth_state = AUTH_READ; ws->nmsgs = nmsgs; ws->msgs = msgs; memset (ws->plaintext_passwd, 0, sizeof(ws->plaintext_passwd)); memset (ws->plaintext_passwd_char_size, 0, sizeof(ws->plaintext_passwd_char_size)); memset (ws->censored_passwd, 0, sizeof(ws->censored_passwd)); ws->unlock_button_state.disabled_p = False; gui_main_loop (ws, False, False); if (ws->auth_state != AUTH_FINISHED) return True; /* Timed out or canceled */ /* Find the (at most one) input field in the previous batch and return the entered plaintext to it. */ for (i = 0; i < nmsgs; i++) { if (msgs[i].type == AUTH_MSGTYPE_PROMPT_ECHO || msgs[i].type == AUTH_MSGTYPE_PROMPT_NOECHO) { if (resp[i].response) abort(); resp[i].response = strdup(ws->plaintext_passwd); } } ws->nmsgs = 0; ws->msgs = 0; return False; } /* To retain this across multiple calls from PAM to xscreensaver_auth_conv. */ window_state *global_ws = 0; /* The authentication conversation function. Like a PAM conversation function, this accepts multiple messages in a single round. We can only do one text entry field in the dialog at a time, so if there is more than one entry, multiple dialogs will be used. PAM might call this multiple times before authenticating. We are unable to combine multiple messages onto a single dialog if PAM splits them between calls to this function. Returns True on success. If the user timed out or cancelled, we just exit. */ Bool xscreensaver_auth_conv (void *closure, int nmsgs, const auth_message *msgs, auth_response **resp) { Widget root_widget = (Widget) closure; int i; int prev_msg = 0; int field_count = 0; auth_response *responses; window_state *ws = global_ws; if (!ws) ws = global_ws = window_init (root_widget, False); responses = calloc (nmsgs, sizeof(*responses)); if (!responses) abort(); for (i = 0; i < nmsgs; i++) { if (msgs[i].type == AUTH_MSGTYPE_PROMPT_ECHO || msgs[i].type == AUTH_MSGTYPE_PROMPT_NOECHO) { /* A text input field. */ if (field_count > 0) { /* This is the second one -- we must run the dialog on the field collected so far. */ if (dialog_session (ws, i - prev_msg, msgs + prev_msg, responses + prev_msg)) goto END; prev_msg = i; field_count = 0; } field_count++; } } if (prev_msg < i || nmsgs == 0) /* Run the dialog on the stuff that's left. This happens if there was more than one text field. */ dialog_session (ws, i - prev_msg, msgs + prev_msg, responses + prev_msg); END: switch (ws->auth_state) { case AUTH_CANCEL: case AUTH_TIME: /* No need to return to PAM or clean up. We're outta here! Exit with 0 to distinguish it from our "success" or "failure" exit codes. */ destroy_window (ws); exit (0); break; case AUTH_FINISHED: *resp = responses; return True; default: abort(); break; } } /* Called after authentication is complete so that we can present a "nope" dialog if it failed, or snitch on previous failed login attempts. */ void xscreensaver_auth_finished (void *closure, Bool authenticated_p) { Widget root_widget = (Widget) closure; window_state *ws = global_ws; char msg[1024]; int unlock_failures = 0; time_t failure_time = 0; Bool prompted_p = !!ws; /* If this was called without xscreensaver_auth_conv() ever having been called, then either PAM decided that the user is authenticated without a prompt (e.g. a bluetooth fob); or there was an error initializing passwords (e.g., shadow passwords but not setuid.) */ if (!ws) ws = global_ws = window_init (root_widget, False); if (authenticated_p) { /* Read the old failure count, and delete it. */ persistent_auth_status_failure (ws, False, True, &unlock_failures, &failure_time); } else { /* Increment the failure count. */ persistent_auth_status_failure (ws, True, False, &unlock_failures, &failure_time); } /* If we have something to say, put the dialog back up for a few seconds to display it. Otherwise, don't bother. */ if (!authenticated_p && !prompted_p) strcpy (msg, _("Password initialization failed")); else if (!authenticated_p && ws && ws->caps_p) strcpy (msg, _("Authentication failed (Caps Lock?)")); else if (!authenticated_p) strcpy (msg, _("Authentication failed!")); else if (authenticated_p && unlock_failures > 0) { time_t now = time ((time_t *) 0); int sec = now - failure_time; int min = (sec + 30) / 60; int hours = (min + 30) / 60; int days = (hours + 12) / 24; char ago[100]; int warning_slack = get_integer_resource (ws->dpy, "authWarningSlack", "AuthWarningSlack"); if (sec < warning_slack) { if (verbose_p) fprintf (stderr, "%s: ignoring recent unlock failures:" " %d within %d sec\n", blurb(), unlock_failures, warning_slack); goto END; } else if (days > 1) sprintf (ago, _("%d days ago"), days); else if (hours > 1) sprintf (ago, _("%d hours ago"), hours); else if (min > 1) sprintf (ago, _("%d minutes ago"), min); else sprintf (ago, _("just now")); if (unlock_failures == 1) sprintf (msg, _("There has been 1 failed login attempt, %s."), ago); else sprintf (msg, _("There have been %d failed login attempts, oldest %s."), unlock_failures, ago); } else { /* No need to pop up a window. Authenticated, and there are no previous failures to report. */ goto END; } if (!*msg) abort(); ws->body_label = strdup (msg); ws->auth_state = AUTH_NOTIFY; gui_main_loop (ws, False, True); END: destroy_window (global_ws); } void xscreensaver_splash (void *closure) { Widget root_widget = (Widget) closure; window_state *ws = window_init (root_widget, True); ws->auth_state = AUTH_READ; gui_main_loop (ws, True, False); destroy_window (ws); exit (0); }