1#!/usr/local/bin/python3.8
2# coding: utf-8
3
4import gi
5
6from gi.repository import Gtk, Gdk, GObject, CScreensaver, Gio
7import traceback
8
9
10from util import utils, trackers
11import status
12import singletons
13from baseWindow import BaseWindow
14from widgets.framedImage import FramedImage
15from passwordEntry import PasswordEntry
16from pamhelper.authClient import AuthClient
17from widgets.transparentButton import TransparentButton
18
19class UnlockDialog(BaseWindow):
20    """
21    The main widget for the unlock dialog - this is a direct child of
22    the Stage's GtkOverlay.
23
24    It has a number of parts, namely:
25        - The user face image.
26        - The user's real name (or username if the real name is unavailable)
27        - The password entry widget
28        - Unlock and Switch User buttons
29        - A caps lock warning label
30        - An invalid password error label
31    """
32    __gsignals__ = {
33        'inhibit-timeout': (GObject.SignalFlags.RUN_LAST, None, ()),
34        'uninhibit-timeout': (GObject.SignalFlags.RUN_LAST, None, ()),
35        'authenticate-success': (GObject.SignalFlags.RUN_LAST, None, ()),
36        'authenticate-failure': (GObject.SignalFlags.RUN_LAST, None, ()),
37        'authenticate-cancel': (GObject.SignalFlags.RUN_LAST, None, ())
38    }
39
40    def __init__(self):
41        super(UnlockDialog, self).__init__()
42
43        settings = Gio.Settings.new("org.cinnamon.desktop.lockdown")
44
45        self.set_halign(Gtk.Align.CENTER)
46        self.set_valign(Gtk.Align.CENTER)
47
48        self.real_name = None
49        self.user_name = None
50
51        self.bounce_rect = None
52        self.bounce_count = 0
53
54        self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
55        self.box.get_style_context().add_class("unlockbox")
56        self.add(self.box)
57
58        self.face_image = FramedImage(status.screen.get_low_res_mode())
59        self.face_image.set_halign(Gtk.Align.CENTER)
60        self.face_image.get_style_context().add_class("faceimage")
61        self.face_image.set_no_show_all(True)
62
63        self.box.pack_start(self.face_image, False, False, 10)
64
65        self.realname_label = Gtk.Label(None)
66        self.realname_label.set_alignment(0, 0.5)
67        self.realname_label.set_halign(Gtk.Align.CENTER)
68
69        self.box.pack_start(self.realname_label, False, False, 10)
70
71        self.entry_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
72
73        self.box.pack_start(self.entry_box, True, True, 2)
74
75        self.password_entry = PasswordEntry()
76
77        trackers.con_tracker_get().connect(self.password_entry,
78                                           "changed",
79                                           self.on_password_entry_text_changed)
80
81        trackers.con_tracker_get().connect(self.password_entry,
82                                           "button-press-event",
83                                           self.on_password_entry_button_press)
84
85        trackers.con_tracker_get().connect(self.password_entry,
86                                           "activate",
87                                           self.on_auth_enter_key)
88
89        self.entry_box.pack_start(self.password_entry, False, False, 15)
90
91        button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
92        self.entry_box.pack_end(button_box, False, False, 0)
93
94        self.auth_unlock_button = TransparentButton("screensaver-unlock-symbolic", Gtk.IconSize.LARGE_TOOLBAR)
95        self.auth_unlock_button.set_tooltip_text(_("Unlock"))
96        trackers.con_tracker_get().connect(self.auth_unlock_button,
97                                           "clicked",
98                                           self.on_unlock_clicked)
99
100        button_box.pack_start(self.auth_unlock_button, False, False, 4)
101
102        status.focusWidgets = [self.password_entry, self.auth_unlock_button]
103
104        if not settings.get_boolean("disable-user-switching"):
105            self.auth_switch_button = TransparentButton("screensaver-switch-users-symbolic", Gtk.IconSize.LARGE_TOOLBAR)
106            self.auth_switch_button.set_tooltip_text(_("Switch User"))
107            trackers.con_tracker_get().connect(self.auth_switch_button, "clicked", self.on_switch_user_clicked)
108            button_box.pack_start(self.auth_switch_button, False, False, 4)
109            status.focusWidgets.append(self.auth_switch_button)
110
111        vbox_messages = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
112
113        self.capslock_label = Gtk.Label("")
114        self.capslock_label.get_style_context().add_class("caps-message")
115        self.capslock_label.set_alignment(0.5, 0.5)
116        vbox_messages.pack_start(self.capslock_label, False, False, 2)
117
118        self.auth_message_label = Gtk.Label("")
119        self.auth_message_label.get_style_context().add_class("auth-message")
120        self.auth_message_label.set_alignment(0.5, 0.5)
121        vbox_messages.pack_start(self.auth_message_label, False, False, 2)
122
123        self.box.pack_start(vbox_messages, False, False, 0)
124
125        self.real_name = utils.get_user_display_name()
126        self.user_name = utils.get_user_name()
127
128        self.update_realname_label()
129
130        self.account_client = singletons.AccountsServiceClient
131        if self.account_client.is_loaded:
132            self.on_account_client_loaded(self.account_client)
133        else:
134            trackers.con_tracker_get().connect(self.account_client,
135                                               "account-loaded",
136                                               self.on_account_client_loaded)
137
138        self.keymap = Gdk.Keymap.get_default()
139
140        trackers.con_tracker_get().connect(self.keymap,
141                                           "state-changed",
142                                           self.keymap_handler)
143
144        self.keymap_handler(self.keymap)
145
146        self.auth_client = AuthClient()
147
148        trackers.con_tracker_get().connect(self.auth_client,
149                                           "auth-success",
150                                           self.on_authentication_success)
151        trackers.con_tracker_get().connect(self.auth_client,
152                                           "auth-failure",
153                                           self.on_authentication_failure)
154        trackers.con_tracker_get().connect(self.auth_client,
155                                           "auth-cancel",
156                                           self.on_authentication_cancelled)
157        trackers.con_tracker_get().connect(self.auth_client,
158                                           "auth-busy",
159                                           self.on_authentication_busy_changed)
160        trackers.con_tracker_get().connect(self.auth_client,
161                                           "auth-prompt",
162                                           self.on_authentication_prompt_changed)
163
164        self.box.show_all()
165
166    def initialize_auth_client(self):
167        return self.auth_client.initialize()
168
169    def cancel_auth_client(self):
170        self.clear_entry()
171
172        self.auth_client.cancel()
173
174    def on_authentication_success(self, auth_client):
175        self.clear_entry()
176        self.emit("authenticate-success")
177
178    def on_authentication_failure(self, auth_client):
179        """
180        Called upon authentication failure, clears the password, sets an error message,
181        and refocuses the password entry.
182        """
183        self.clear_entry()
184        self.auth_message_label.set_text(_("Incorrect password"))
185
186        self.emit("authenticate-failure")
187        self.emit("uninhibit-timeout")
188
189    def on_authentication_cancelled(self, auth_client):
190        self.emit("authenticate-cancel")
191
192    def on_authentication_busy_changed(self, auth_client, busy):
193        if busy:
194            self.auth_message_label.set_text("")
195            self.clear_entry()
196            self.entry_box.set_sensitive(False)
197            self.password_entry.start_progress()
198            self.password_entry.set_placeholder_text (_("Checking..."))
199        else:
200            self.entry_box.set_sensitive(True)
201            self.password_entry.stop_progress()
202            self.password_entry.set_placeholder_text (self.password_entry.placeholder_text)
203
204    def on_authentication_prompt_changed(self, auth_client, prompt):
205        if "password:" in prompt.lower():
206            prompt = _("Please enter your password...")
207        else:
208            prompt = prompt.replace(":", "")
209
210        self.password_entry.placeholder_text = prompt
211        self.password_entry.set_placeholder_text(self.password_entry.placeholder_text)
212
213    def cancel(self):
214        """
215        Clears the auth message text if we have any.
216        """
217        self.auth_message_label.set_text("")
218
219    def queue_key_event(self, event):
220        """
221        Takes a propagated key event from the stage and passes it to the entry widget,
222        possibly queueing up the first character of the password.
223        """
224        if not self.password_entry.get_realized():
225            self.password_entry.realize()
226            self.password_entry.grab_focus()
227
228        self.password_entry.event(event)
229
230    def keymap_handler(self, keymap):
231        """
232        Handler for the GdkKeymap changing - updates our capslock indicator label.
233        """
234        if keymap.get_caps_lock_state():
235            self.capslock_label.set_text(_("You have the Caps Lock key on."))
236        else:
237            self.capslock_label.set_text("")
238
239    def on_account_client_loaded(self, client):
240        """
241        Handler for the AccountsService - requests the user real name and .face image.
242        """
243        if client.get_real_name() != None:
244            self.real_name = client.get_real_name()
245            self.update_realname_label()
246
247        if client.get_face_path() != None:
248            self.face_image.set_from_path(client.get_face_path())
249            self.face_image.show()
250
251    def on_password_entry_text_changed(self, editable):
252        """
253        Handler for the password entry text changing - this controls the sensitivity
254        of the unlock button, as well as returning visual focus to the entry any time
255        a key event is received.
256        """
257
258        if not self.password_entry.has_focus():
259            self.password_entry.grab_focus()
260
261    def on_password_entry_button_press(self, widget, event):
262        """
263        Prevents the usual copy/paste popup when right-clicking the PasswordEntry.
264        """
265        if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
266            return Gdk.EVENT_STOP
267
268        return Gdk.EVENT_PROPAGATE
269
270    def on_unlock_clicked(self, button=None):
271        """
272        Callback for the unlock button.  Activates the 'progress' animation
273        in the GtkEntry, and attempts to authenticate the password.  During this
274        time, we also inhibit the unlock timeout, so we don't fade out while waiting
275        for an authentication result (highly unlikely.)
276        """
277        self.emit("inhibit-timeout")
278
279        text = self.password_entry.get_text()
280
281        # We must end with a newline, fgets relies upon that to continue.
282        if text[-1:] != "\n":
283            text += "\n"
284
285        self.auth_client.message_to_child(text)
286
287    def on_auth_enter_key(self, widget):
288        """
289        Implicitly activates the unlock button when the Enter/Return key is pressed.
290        """
291
292        self.on_unlock_clicked()
293
294    def on_switch_user_clicked(self, widget):
295        """
296        Callback for the switch-user button.
297        """
298        utils.do_user_switch()
299
300    def clear_entry(self):
301        """
302        Clear the password entry widget.
303        """
304        self.password_entry.set_text("")
305
306    def update_realname_label(self):
307        """
308        Updates the name label to the current real_name.
309        """
310        self.realname_label.set_text(self.real_name)
311
312    def blink(self):
313        GObject.timeout_add(75, self.on_blink_tick)
314
315    def on_blink_tick(self, data=None):
316        window = self.get_window()
317
318        if window == None:
319            return False
320
321        x, y = window.get_position()
322
323        if self.bounce_count < 6:
324            if self.bounce_count % 2 == 0:
325                y += 6
326            else:
327                y -= 6
328            self.get_window().move(x, y)
329            self.queue_draw()
330
331            self.bounce_count += 1
332
333            return True
334
335        self.bounce_count = 0
336        return False
337