1#!/usr/local/bin/python3.8
2
3import os
4import pwd
5import grp
6import gettext
7import shutil
8import re
9import subprocess
10from random import randint
11from setproctitle import setproctitle
12
13import PIL
14from PIL import Image
15import gi
16gi.require_version("Gtk", "3.0")
17gi.require_version("AccountsService", "1.0")
18from gi.repository import Gtk, GObject, Gio, GdkPixbuf, AccountsService, GLib
19
20gettext.install("cinnamon", "/usr/local/share/locale")
21
22class PrivHelper(object):
23    """A helper for performing temporary privilege drops. Necessary for
24    security when accessing user controlled files as root."""
25
26    def __init__(self):
27
28        self.orig_uid = os.getuid()
29        self.orig_gid = os.getgid()
30        self.orig_groups = os.getgroups()
31
32    def drop_privs(self, user):
33
34        uid = user.get_uid()
35        # the user's main group id
36        gid = pwd.getpwuid(uid).pw_gid
37
38        # initialize the user's supplemental groups and main group
39        os.initgroups(user.get_user_name(), gid)
40        os.setegid(gid)
41        os.seteuid(uid)
42
43    def restore_privs(self):
44
45        os.seteuid(self.orig_uid)
46        os.setegid(self.orig_gid)
47        os.setgroups(self.orig_groups)
48
49priv_helper = PrivHelper()
50
51(INDEX_USER_OBJECT, INDEX_USER_PICTURE, INDEX_USER_DESCRIPTION) = range(3)
52(INDEX_GID, INDEX_GROUPNAME) = range(2)
53
54class GroupDialog (Gtk.Dialog):
55    def __init__ (self, label, value, parent = None):
56        super(GroupDialog, self).__init__(None, parent)
57
58        try:
59            self.set_modal(True)
60            self.set_skip_taskbar_hint(True)
61            self.set_skip_pager_hint(True)
62            self.set_title("")
63
64            table = DimmedTable()
65            table.add_labels([label])
66
67            self.entry = Gtk.Entry()
68            self.entry.set_text(value)
69            self.entry.connect("changed", self._on_entry_changed)
70            table.add_controls([self.entry])
71
72            self.set_border_width(6)
73
74            box = self.get_content_area()
75            box.add(table)
76            self.show_all()
77
78            self.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, _("OK"), Gtk.ResponseType.OK, )
79            self.set_response_sensitive(Gtk.ResponseType.OK, False)
80
81        except Exception as detail:
82            print(detail)
83
84    def _on_entry_changed(self, entry):
85        name = entry.get_text()
86        if " " in name or name.lower() != name:
87            entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "dialog-warning-symbolic")
88            entry.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("The group name cannot contain upper-case or space characters"))
89            self.set_response_sensitive(Gtk.ResponseType.OK, False)
90        else:
91            entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, None)
92            self.set_response_sensitive(Gtk.ResponseType.OK, True)
93
94        if entry.get_text() == "":
95            self.set_response_sensitive(Gtk.ResponseType.OK, False)
96
97class DimmedTable (Gtk.Table):
98    def __init__ (self):
99        super(DimmedTable, self).__init__()
100        self.set_border_width(6)
101        self.set_row_spacings(8)
102        self.set_col_spacings(15)
103
104    def add_labels(self, texts):
105        row = 0
106        for text in texts:
107            if text != None:
108                label = Gtk.Label(text)
109                label.set_alignment(1, 0.5)
110                label.get_style_context().add_class("dim-label")
111                self.attach(label, 0, 1, row, row+1, xoptions=Gtk.AttachOptions.EXPAND|Gtk.AttachOptions.FILL)
112            row = row + 1
113
114    def add_controls(self, controls):
115        row = 0
116        for control in controls:
117            self.attach(control, 1, 2, row, row+1)
118            row = row + 1
119
120
121class EditableEntry (Gtk.Notebook):
122
123    __gsignals__ = {
124        'changed': (GObject.SIGNAL_RUN_FIRST, None,
125                    (str,))
126    }
127
128    PAGE_BUTTON = 0
129    PAGE_ENTRY = 1
130
131    def __init__ (self):
132        super(EditableEntry, self).__init__()
133
134        self.label = Gtk.Label()
135        self.entry = Gtk.Entry()
136        self.button = Gtk.Button()
137
138        self.button.set_alignment(0.0, 0.5)
139        self.button.set_relief(Gtk.ReliefStyle.NONE)
140        self.append_page(self.button, None);
141        self.append_page(self.entry, None);
142        self.set_current_page(0)
143        self.set_show_tabs(False)
144        self.set_show_border(False)
145        self.editable = False
146        self.show_all()
147
148        self.button.connect("released", self._on_button_clicked)
149        self.button.connect("activate", self._on_button_clicked)
150        self.entry.connect("activate", self._on_entry_validated)
151        self.entry.connect("changed", self._on_entry_changed)
152
153    def set_text(self, text):
154        self.button.set_label(text)
155        self.entry.set_text(text)
156
157    def _on_button_clicked(self, button):
158        self.set_editable(True)
159
160    def _on_entry_validated(self, entry):
161        self.set_editable(False)
162        self.emit("changed", entry.get_text())
163
164    def _on_entry_changed(self, entry):
165        self.button.set_label(entry.get_text())
166
167    def set_editable(self, editable):
168        if (editable):
169            self.set_current_page(EditableEntry.PAGE_ENTRY)
170        else:
171            self.set_current_page(EditableEntry.PAGE_BUTTON)
172        self.editable = editable
173
174    def set_tooltip_text(self, tooltip):
175        self.button.set_tooltip_text(tooltip)
176
177    def get_editable(self):
178        return self.editable
179
180    def get_text(self):
181        return self.entry.get_text()
182
183class PasswordDialog(Gtk.Dialog):
184
185    def __init__ (self, user, password_mask, group_mask, parent = None):
186        super(PasswordDialog, self).__init__(None, parent)
187
188        self.user = user
189        self.password_mask = password_mask
190        self.group_mask = group_mask
191
192        self.set_modal(True)
193        self.set_skip_taskbar_hint(True)
194        self.set_skip_pager_hint(True)
195        self.set_title(_("Change Password"))
196
197        table = DimmedTable()
198        table.add_labels([_("New password"), None, _("Confirm password")])
199
200        self.new_password = Gtk.Entry()
201        self.new_password.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "view-refresh-symbolic")
202        self.new_password.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("Generate a password"))
203        self.new_password.connect("icon-release", self._on_new_password_icon_released)
204        self.new_password.connect("changed", self._on_passwords_changed)
205        table.attach(self.new_password, 1, 3, 0, 1)
206
207        self.strengh_indicator = Gtk.ProgressBar()
208        self.strengh_indicator.set_tooltip_text(_("Your new password needs to be at least 8 characters long"))
209        self.strengh_indicator.set_fraction(0.0)
210        table.attach(self.strengh_indicator, 1, 2, 1, 2, xoptions=Gtk.AttachOptions.EXPAND|Gtk.AttachOptions.FILL)
211        self.strengh_indicator.set_size_request(-1, 1)
212
213        self.strengh_label = Gtk.Label()
214        self.strengh_label.set_tooltip_text(_("Your new password needs to be at least 8 characters long"))
215        self.strengh_label.set_alignment(1, 0.5)
216        table.attach(self.strengh_label, 2, 3, 1, 2)
217
218        self.confirm_password = Gtk.Entry()
219        self.confirm_password.connect("changed", self._on_passwords_changed)
220        table.attach(self.confirm_password, 1, 3, 2, 3)
221
222        self.show_password = Gtk.CheckButton(_("Show password"))
223        self.show_password.connect('toggled', self._on_show_password_toggled)
224        table.attach(self.show_password, 1, 3, 3, 4)
225
226        self.set_border_width(6)
227
228        box = self.get_content_area()
229        box.add(table)
230        self.show_all()
231
232        self.infobar = Gtk.InfoBar()
233        self.infobar.set_message_type(Gtk.MessageType.ERROR)
234        label = Gtk.Label(_("An error occurred. Your password was not changed."))
235        content = self.infobar.get_content_area()
236        content.add(label)
237        table.attach(self.infobar, 0, 3, 4, 5)
238
239        self.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, _("Change"), Gtk.ResponseType.OK, )
240
241        self.set_passwords_visibility()
242        self.set_response_sensitive(Gtk.ResponseType.OK, False)
243        self.infobar.hide()
244
245        self.connect("response", self._on_response)
246
247    def _on_response(self, dialog, response_id):
248        if response_id == Gtk.ResponseType.OK:
249            self.change_password()
250        else:
251            self.destroy()
252
253    def change_password(self):
254        newpass = self.new_password.get_text()
255        self.user.set_password(newpass, "")
256        mask = self.group_mask.get_text()
257        if "nopasswdlogin" in mask:
258            subprocess.call(["gpasswd", "-d", self.user.get_user_name(), "nopasswdlogin"])
259            mask = mask.split(", ")
260            mask.remove("nopasswdlogin")
261            mask = ", ".join(mask)
262            self.group_mask.set_text(mask)
263            self.password_mask.set_text('\u2022\u2022\u2022\u2022\u2022\u2022')
264        self.destroy()
265
266    def set_passwords_visibility(self):
267        visible = self.show_password.get_active()
268        self.new_password.set_visibility(visible)
269        self.confirm_password.set_visibility(visible)
270
271    def _on_new_password_icon_released(self, widget, icon_pos, event):
272        self.infobar.hide()
273        self.show_password.set_active(True)
274        characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-"
275        newpass = ""
276        for i in range (8):
277            index = randint(0, len(characters) -1)
278            newpass = newpass + characters[index]
279
280        self.new_password.set_text(newpass)
281        self.confirm_password.set_text(newpass)
282        self.check_passwords()
283
284    def _on_show_password_toggled(self, widget):
285        self.set_passwords_visibility()
286
287    # Based on setPasswordStrength() in Mozilla Seamonkey, which is tri-licensed under MPL 1.1, GPL 2.0, and LGPL 2.1.
288    # Forked from Ubiquity validation.py
289    def password_strength(self, password):
290        upper = lower = digit = symbol = 0
291        for char in password:
292            if char.isdigit():
293                digit += 1
294            elif char.islower():
295                lower += 1
296            elif char.isupper():
297                upper += 1
298            else:
299                symbol += 1
300        length = len(password)
301
302        length = min(length,4)
303        digit = min(digit,3)
304        upper = min(upper,3)
305        symbol = min(symbol,3)
306        strength = (
307            ((length * 0.1) - 0.2) +
308            (digit * 0.1) +
309            (symbol * 0.15) +
310            (upper * 0.1))
311        if strength > 1:
312            strength = 1
313        if strength < 0:
314            strength = 0
315        return strength
316
317    def _on_passwords_changed(self, widget):
318        self.infobar.hide()
319        new_password = self.new_password.get_text()
320        confirm_password = self.confirm_password.get_text()
321        strength = self.password_strength(new_password)
322        if new_password != confirm_password:
323            self.confirm_password.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "dialog-warning-symbolic")
324            self.confirm_password.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("Passwords do not match"))
325        else:
326            self.confirm_password.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, None)
327        if len(new_password) < 8:
328            self.strengh_label.set_text(_("Too short"))
329            self.strengh_indicator.set_fraction(0.0)
330        elif strength < 0.5:
331            self.strengh_label.set_text(_("Weak"))
332            self.strengh_indicator.set_fraction(0.2)
333        elif strength < 0.75:
334            self.strengh_label.set_text(_("Fair"))
335            self.strengh_indicator.set_fraction(0.4)
336        elif strength < 0.9:
337            self.strengh_label.set_text(_("Good"))
338            self.strengh_indicator.set_fraction(0.6)
339        else:
340            self.strengh_label.set_text(_("Strong"))
341            self.strengh_indicator.set_fraction(1.0)
342
343        self.check_passwords()
344
345    def check_passwords(self):
346        new_password = self.new_password.get_text()
347        confirm_password = self.confirm_password.get_text()
348        if len(new_password) >= 8 and new_password == confirm_password:
349            self.set_response_sensitive(Gtk.ResponseType.OK, True)
350        else:
351            self.set_response_sensitive(Gtk.ResponseType.OK, False)
352
353class NewUserDialog(Gtk.Dialog):
354
355    def __init__ (self, parent = None):
356        super(NewUserDialog, self).__init__(None, parent)
357
358        try:
359            self.set_modal(True)
360            self.set_skip_taskbar_hint(True)
361            self.set_skip_pager_hint(True)
362            self.set_title("")
363
364            self.account_type_combo = Gtk.ComboBoxText()
365            self.account_type_combo.append_text(_("Standard"))
366            self.account_type_combo.append_text(_("Administrator"))
367            self.account_type_combo.set_active(0)
368
369            self.realname_entry = Gtk.Entry()
370            self.realname_entry.connect("changed", self._on_info_changed)
371
372            self.username_entry = Gtk.Entry()
373            self.username_entry.connect("changed", self._on_info_changed)
374
375            label = Gtk.Label()
376            label.set_markup(_("The username must consist of only:\n    - lower case letters (a-z)\n    - numerals (0-9)\n    - '.', '-', and '_' characters"))
377
378            table = DimmedTable()
379            table.add_labels([_("Account Type"), _("Full Name"), _("Username")])
380            table.add_controls([self.account_type_combo, self.realname_entry, self.username_entry])
381
382            self.set_border_width(6)
383
384            box = self.get_content_area()
385            box.add(table)
386            box.add(label)
387            self.show_all()
388
389            self.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, _("Add"), Gtk.ResponseType.OK, )
390            self.set_response_sensitive(Gtk.ResponseType.OK, False)
391
392        except Exception as detail:
393            print(detail)
394
395    def _on_info_changed(self, widget):
396        fullname = self.realname_entry.get_text()
397        username = self.username_entry.get_text()
398        valid = True
399        if re.search('[^a-z0-9_.-]', username):
400            self.username_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "dialog-warning-symbolic")
401            self.username_entry.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("Invalid username"))
402            valid = False
403        else:
404            self.username_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, None)
405        if username == "" or fullname == "":
406            valid = False
407
408        self.set_response_sensitive(Gtk.ResponseType.OK, valid)
409
410class GroupsDialog(Gtk.Dialog):
411
412    def __init__ (self, username, parent = None):
413        super(GroupsDialog, self).__init__(None, parent)
414
415        try:
416            self.set_modal(True)
417            self.set_skip_taskbar_hint(True)
418            self.set_skip_pager_hint(True)
419            self.set_title("")
420            self.set_default_size(200, 480)
421
422            scrolled = Gtk.ScrolledWindow()
423            viewport = Gtk.Viewport()
424            vbox = Gtk.VBox()
425            self.checkboxes = []
426            groups = sorted(grp.getgrall(), key=lambda x: x[0], reverse=False)
427            for group in groups:
428                checkbox = Gtk.CheckButton(group[0])
429                self.checkboxes.append(checkbox)
430                vbox.add(checkbox)
431                if username in group[3]:
432                    checkbox.set_active(True)
433
434            viewport.add(vbox)
435            scrolled.add(viewport)
436            self.set_border_width(6)
437
438            box = self.get_content_area()
439            box.pack_start(scrolled, True, True, 0)
440            self.show_all()
441
442            self.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, _("OK"), Gtk.ResponseType.OK, )
443
444        except Exception as detail:
445            print(detail)
446
447    def get_selected_groups(self):
448        groups = []
449        for checkbox in self.checkboxes:
450            if checkbox.get_active():
451                groups.append(checkbox.get_label())
452        return groups
453
454class Module:
455    def __init__(self):
456        try:
457            self.builder = Gtk.Builder()
458            self.builder.set_translation_domain('cinnamon') # let it translate!
459            self.builder.add_from_file("/usr/local/share/cinnamon/cinnamon-settings-users/cinnamon-settings-users.ui")
460            self.window = self.builder.get_object("main_window")
461            self.window.connect("destroy", Gtk.main_quit)
462
463            self.window.set_title(_("Users and Groups"))
464            self.builder.get_object("label_users").set_label(_("Users"))
465            self.builder.get_object("label_groups").set_label(_("Groups"))
466
467            self.builder.get_object("button_add_user").connect("clicked", self.on_user_addition)
468            self.builder.get_object("button_delete_user").connect("clicked", self.on_user_deletion)
469            self.builder.get_object("button_add_group").connect("clicked", self.on_group_addition)
470            self.builder.get_object("button_edit_group").connect("clicked", self.on_group_edition)
471            self.builder.get_object("button_delete_group").connect("clicked", self.on_group_deletion)
472
473            self.users = Gtk.TreeStore(object, GdkPixbuf.Pixbuf, str)
474            self.users.set_sort_column_id(2, Gtk.SortType.ASCENDING)
475
476            self.groups = Gtk.TreeStore(int, str)
477            self.groups.set_sort_column_id(1, Gtk.SortType.ASCENDING)
478
479            self.users_treeview = self.builder.get_object("treeview_users")
480            self.users_treeview.set_rules_hint(True)
481
482            self.groups_treeview = self.builder.get_object("treeview_groups")
483
484            self.users_treeview.get_selection().connect("changed", self.on_user_selection)
485            self.groups_treeview.get_selection().connect("changed", self.on_group_selection)
486
487            column = Gtk.TreeViewColumn()
488            cell = Gtk.CellRendererPixbuf()
489            column.pack_start(cell, True)
490            column.add_attribute(cell, 'pixbuf', INDEX_USER_PICTURE)
491            cell.set_property('ypad', 1)
492            self.users_treeview.append_column(column)
493
494            column = Gtk.TreeViewColumn()
495            cell = Gtk.CellRendererText()
496            column.pack_start(cell, True)
497            column.add_attribute(cell, 'markup', INDEX_USER_DESCRIPTION)
498            self.users_treeview.append_column(column)
499
500            column = Gtk.TreeViewColumn()
501            cell = Gtk.CellRendererText()
502            column.pack_start(cell, True)
503            column.add_attribute(cell, 'text', INDEX_GROUPNAME)
504            column.set_sort_column_id(1)
505            self.groups_treeview.append_column(column)
506
507            self.builder.get_object("button_delete_user").set_sensitive(False)
508            self.builder.get_object("button_edit_group").set_sensitive(False)
509            self.builder.get_object("button_delete_group").set_sensitive(False)
510
511            self.face_button = Gtk.Button()
512            self.face_image = Gtk.Image()
513            self.face_image.set_size_request(96, 96)
514            self.face_button.set_image(self.face_image)
515            self.face_image.set_from_file("/usr/local/share/cinnamon/faces/user-generic.png")
516            self.face_button.set_alignment(0.0, 0.5)
517            self.face_button.set_tooltip_text(_("Click to change the picture"))
518
519            self.menu = Gtk.Menu()
520
521            separator = Gtk.SeparatorMenuItem()
522            face_browse_menuitem = Gtk.MenuItem(_("Browse for more pictures..."))
523            face_browse_menuitem.connect('activate', self._on_face_browse_menuitem_activated)
524            self.face_button.connect("button-release-event", self.menu_display)
525
526            row = 0
527            col = 0
528            num_cols = 4
529            face_dirs = ["/usr/local/share/cinnamon/faces"]
530            for face_dir in face_dirs:
531                if os.path.exists(face_dir):
532                    pictures = sorted(os.listdir(face_dir))
533                    for picture in pictures:
534                        path = os.path.join(face_dir, picture)
535                        file = Gio.File.new_for_path(path)
536                        file_icon = Gio.FileIcon.new(file)
537                        image = Gtk.Image.new_from_gicon (file_icon, Gtk.IconSize.DIALOG)
538                        menuitem = Gtk.MenuItem()
539                        menuitem.add(image)
540                        menuitem.connect('activate', self._on_face_menuitem_activated, path)
541                        self.menu.attach(menuitem, col, col+1, row, row+1)
542                        col = (col+1) % num_cols
543                        if (col == 0):
544                            row = row + 1
545
546            row = row + 1
547
548            self.menu.attach(separator, 0, 4, row, row+1)
549            self.menu.attach(face_browse_menuitem, 0, 4, row+2, row+3)
550
551            self.account_type_combo = Gtk.ComboBoxText()
552            self.account_type_combo.append_text(_("Standard"))
553            self.account_type_combo.append_text(_("Administrator"))
554            self.account_type_combo.connect("changed", self._on_accounttype_changed)
555
556            self.realname_entry = EditableEntry()
557            self.realname_entry.connect("changed", self._on_realname_changed)
558            self.realname_entry.set_tooltip_text(_("Click to change the name"))
559
560            self.password_mask = Gtk.Label()
561            self.password_mask.set_alignment(0.0, 0.5)
562            self.password_button = Gtk.Button()
563            self.password_button.add(self.password_mask)
564            self.password_button.set_relief(Gtk.ReliefStyle.NONE)
565            self.password_button.set_tooltip_text(_("Click to change the password"))
566            self.password_button.connect('activate', self._on_password_button_clicked)
567            self.password_button.connect('released', self._on_password_button_clicked)
568
569            self.groups_label = Gtk.Label()
570            self.groups_label.set_line_wrap(True)
571            self.groups_label.set_alignment(0, 0.5)
572            self.groups_button = Gtk.Button()
573            self.groups_button.add(self.groups_label)
574            self.groups_button.set_relief(Gtk.ReliefStyle.NONE)
575            self.groups_button.set_tooltip_text(_("Click to change the groups"))
576            self.groups_button.connect("clicked", self._on_groups_button_clicked)
577
578            box = Gtk.Box()
579            box.pack_start(self.face_button, False, False, 0)
580
581            table = DimmedTable()
582            table.add_labels([_("Picture"), _("Account Type"), _("Name"), _("Password"), _("Groups")])
583            table.add_controls([box, self.account_type_combo, self.realname_entry, self.password_button, self.groups_button])
584
585            self.builder.get_object("box_users").add(table)
586
587            self.accountService = AccountsService.UserManager.get_default()
588            self.accountService.connect('notify::is-loaded', self.on_accounts_service_loaded)
589
590            self.load_groups()
591
592            self.window.show_all()
593
594            self.builder.get_object("box_users").hide()
595
596        except Exception as detail:
597            print(detail)
598
599    def _on_password_button_clicked(self, widget):
600        model, treeiter = self.users_treeview.get_selection().get_selected()
601        if treeiter != None:
602            user = model[treeiter][INDEX_USER_OBJECT]
603            dialog = PasswordDialog(user, self.password_mask, self.groups_label, self.window)
604            response = dialog.run()
605
606    def _on_groups_button_clicked(self, widget):
607        model, treeiter = self.users_treeview.get_selection().get_selected()
608        if treeiter != None:
609            user = model[treeiter][INDEX_USER_OBJECT]
610            dialog = GroupsDialog(user.get_user_name(), self.window)
611            response = dialog.run()
612            if response == Gtk.ResponseType.OK:
613                groups = dialog.get_selected_groups()
614                subprocess.call(["usermod", user.get_user_name(), "-G", ",".join(groups)])
615                groups.sort()
616                self.groups_label.set_text(", ".join(groups))
617            dialog.destroy()
618
619    def _on_accounttype_changed(self, combobox):
620        model, treeiter = self.users_treeview.get_selection().get_selected()
621        if treeiter != None:
622            user = model[treeiter][INDEX_USER_OBJECT]
623            if self.account_type_combo.get_active() == 1:
624                user.set_account_type(AccountsService.UserAccountType.ADMINISTRATOR)
625            else:
626                user.set_account_type(AccountsService.UserAccountType.STANDARD)
627
628            groups = []
629            for group in grp.getgrall():
630                if user.get_user_name() in group[3]:
631                    groups.append(group[0])
632            groups.sort()
633            self.groups_label.set_text(", ".join(groups))
634
635    def _on_realname_changed(self, widget, text):
636        model, treeiter = self.users_treeview.get_selection().get_selected()
637        if treeiter != None:
638            user = model[treeiter][INDEX_USER_OBJECT]
639            user.set_real_name(text)
640            description = "<b>%s</b>\n%s" % (text, user.get_user_name())
641            model.set_value(treeiter, INDEX_USER_DESCRIPTION, description)
642
643    def _on_face_browse_menuitem_activated(self, menuitem):
644        model, treeiter = self.users_treeview.get_selection().get_selected()
645        if treeiter != None:
646            user = model[treeiter][INDEX_USER_OBJECT]
647            dialog = Gtk.FileChooserDialog(None, None, Gtk.FileChooserAction.OPEN, (_("Cancel"), Gtk.ResponseType.CANCEL, _("Open"), Gtk.ResponseType.OK))
648            filter = Gtk.FileFilter()
649            filter.set_name(_("Images"))
650            filter.add_mime_type("image/*")
651            dialog.add_filter(filter)
652
653            box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
654            self.frame = Gtk.Frame(visible=False, no_show_all=True)
655            preview = Gtk.Image(visible=True)
656
657            box.pack_start(self.frame, False, False, 0)
658            self.frame.add(preview)
659            dialog.set_preview_widget(box)
660            dialog.set_preview_widget_active(True)
661            dialog.set_use_preview_label(False)
662
663            box.set_margin_end(12)
664            box.set_margin_top(12)
665            box.set_size_request(128, -1)
666
667            dialog.connect("update-preview", self.update_preview_cb, preview)
668
669            response = dialog.run()
670            if response == Gtk.ResponseType.OK:
671                path = dialog.get_filename()
672                image = PIL.Image.open(path)
673                image.thumbnail((96, 96), Image.ANTIALIAS)
674                face_path = os.path.join(user.get_home_dir(), ".face")
675                try:
676                    try:
677                        os.remove(face_path)
678                    except OSError:
679                        pass
680                    priv_helper.drop_privs(user)
681                    image.save(face_path, "png")
682                finally:
683                    priv_helper.restore_privs()
684                user.set_icon_file(face_path)
685                self.face_image.set_from_file(face_path)
686                model.set_value(treeiter, INDEX_USER_PICTURE, GdkPixbuf.Pixbuf.new_from_file_at_size(face_path, 48, 48))
687                model.row_changed(model.get_path(treeiter), treeiter)
688
689            dialog.destroy()
690
691    def update_preview_cb (self, dialog, preview):
692        # Different widths make the dialog look really crappy as it resizes -
693        # constrain the width and adjust the height to keep perspective.
694        filename = dialog.get_preview_filename()
695        if filename is not None:
696            if os.path.isfile(filename):
697                try:
698                    pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(filename, 128, 128)
699                    if pixbuf is not None:
700                        preview.set_from_pixbuf(pixbuf)
701                        self.frame.show()
702                        return
703                except GLib.Error as e:
704                    print("Unable to generate preview for file '%s' - %s\n" % (filename, e.message))
705
706        preview.clear()
707        self.frame.hide()
708
709    def _on_face_menuitem_activated(self, menuitem, path):
710        if os.path.exists(path):
711            model, treeiter = self.users_treeview.get_selection().get_selected()
712            if treeiter != None:
713                user = model[treeiter][INDEX_USER_OBJECT]
714                user.set_icon_file(path)
715                self.face_image.set_from_file(path)
716                face_path = os.path.join(user.get_home_dir(), ".face")
717                try:
718                    try:
719                        os.remove(face_path)
720                    except OSError:
721                        pass
722                    priv_helper.drop_privs(user)
723                    shutil.copy(path, face_path)
724                finally:
725                    priv_helper.restore_privs()
726                model.set_value(treeiter, INDEX_USER_PICTURE, GdkPixbuf.Pixbuf.new_from_file_at_size(path, 48, 48))
727                model.row_changed(model.get_path(treeiter), treeiter)
728
729
730    def menu_display(self, widget, event):
731        if event.button == 1:
732            self.menu.popup(None, None, self.popup_menu_below_button, self.face_button, event.button, event.time)
733            self.menu.show_all()
734
735    def popup_menu_below_button (self, *args):
736        # the introspection for GtkMenuPositionFunc seems to change with each Gtk version,
737        # this is a workaround to make sure we get the menu and the widget
738        menu = args[0]
739        widget = args[-1]
740
741        # here I get the coordinates of the button relative to
742        # window (self.window)
743        button_x, button_y = widget.get_allocation().x, widget.get_allocation().y
744
745        # now convert them to X11-relative
746        unused_var, window_x, window_y = widget.get_window().get_origin()
747        x = window_x + button_x
748        y = window_y + button_y
749
750        # now move the menu below the button
751        y += widget.get_allocation().height
752
753        push_in = True # push_in is True so all menu is always inside screen
754        return (x, y, push_in)
755
756    def on_accounts_service_loaded(self, user, param):
757        self.load_users()
758
759    def load_users(self):
760        self.users.clear()
761        users = self.accountService.list_users()
762        for user in users:
763            if os.path.exists(user.get_icon_file()):
764                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(user.get_icon_file(), 48, 48)
765            else:
766                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size("/usr/local/share/cinnamon/faces/user-generic.png", 48, 48)
767            description = "<b>%s</b>\n%s" % (user.get_real_name(), user.get_user_name())
768            piter = self.users.append(None, [user, pixbuf, description])
769        self.users_treeview.set_model(self.users)
770
771    def load_groups(self):
772        self.groups.clear()
773        groups = sorted(grp.getgrall(), key=lambda x: x[0], reverse=False)
774        for group in groups:
775            (gr_name, gr_passwd, gr_gid, gr_mem) = group
776            piter = self.groups.append(None, [gr_gid, gr_name])
777        self.groups_treeview.set_model(self.groups)
778
779#USER CALLBACKS
780
781    def on_user_selection(self, selection):
782        self.password_button.set_sensitive(True)
783        self.password_button.set_tooltip_text("")
784
785        model, treeiter = selection.get_selected()
786        if treeiter != None:
787            user = model[treeiter][INDEX_USER_OBJECT]
788            self.builder.get_object("button_delete_user").set_sensitive(True)
789            self.realname_entry.set_text(user.get_real_name())
790
791            if user.get_password_mode() == AccountsService.UserPasswordMode.REGULAR:
792                self.password_mask.set_text('\u2022\u2022\u2022\u2022\u2022\u2022')
793            elif user.get_password_mode() == AccountsService.UserPasswordMode.NONE:
794                self.password_mask.set_markup("<b>%s</b>" % _("No password set"))
795            else:
796                self.password_mask.set_text(_("Set at login"))
797
798            if user.get_account_type() == AccountsService.UserAccountType.ADMINISTRATOR:
799                self.account_type_combo.set_active(1)
800            else:
801                self.account_type_combo.set_active(0)
802
803            pixbuf = None
804            path = user.get_icon_file()
805            message = ""
806
807            if os.path.exists(path):
808                try:
809                    pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
810                except GLib.Error as e:
811                    message = "Could not load pixbuf from '%s': %s" % (path, e.message)
812                    error = True
813
814                if pixbuf != None:
815                    if pixbuf.get_height() > 96 or pixbuf.get_width() > 96:
816                        try:
817                            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(path, 96, 96)
818                        except GLib.Error as e:
819                            message = "Could not scale pixbuf from '%s': %s" % (path, e.message)
820                            error = True
821
822            if pixbuf:
823                self.face_image.set_from_pixbuf(pixbuf)
824            else:
825                if message != "":
826                    print(message)
827                self.face_image.set_from_file("/usr/local/share/cinnamon/faces/user-generic.png")
828
829            groups = []
830            for group in grp.getgrall():
831                if user.get_user_name() in group[3]:
832                    groups.append(group[0])
833            groups.sort()
834            self.groups_label.set_text(", ".join(groups))
835            self.builder.get_object("box_users").show()
836
837            # Count the number of connections for the currently logged-in user
838            connections = int(subprocess.check_output(["w", "-hs", user.get_user_name()]).decode("utf-8").count("\n"))
839            if connections > 0:
840                self.builder.get_object("button_delete_user").set_sensitive(False)
841                self.builder.get_object("button_delete_user").set_tooltip_text(_("This user is currently logged in"))
842            else:
843                self.builder.get_object("button_delete_user").set_sensitive(True)
844                self.builder.get_object("button_delete_user").set_tooltip_text("")
845
846            if os.path.exists("/home/.ecryptfs/%s" % user.get_user_name()):
847                self.password_button.set_sensitive(False)
848                self.password_button.set_tooltip_text(_("The user's home directory is encrypted. To preserve access to the encrypted directory, only the user should change this password."))
849
850        else:
851            self.builder.get_object("button_delete_user").set_sensitive(False)
852            self.builder.get_object("box_users").hide()
853
854    def on_user_deletion(self, event):
855        model, treeiter = self.users_treeview.get_selection().get_selected()
856        if treeiter != None:
857            user = model[treeiter][INDEX_USER_OBJECT]
858            message = _("Are you sure you want to permanently delete %s and all the files associated with this user?") % user.get_user_name()
859            d = Gtk.MessageDialog(self.window,
860                                  Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
861                                  Gtk.MessageType.QUESTION,
862                                  Gtk.ButtonsType.YES_NO,
863                                  message)
864            d.set_markup(message)
865            d.set_default_response(Gtk.ResponseType.NO)
866            r = d.run()
867            d.destroy()
868            if r == Gtk.ResponseType.YES:
869                result = self.accountService.delete_user(user, True)
870                if result:
871                    model.remove(treeiter)
872                    self.load_groups()
873
874    def on_user_addition(self, event):
875        dialog = NewUserDialog(self.window)
876        response = dialog.run()
877        if response == Gtk.ResponseType.OK:
878            if dialog.account_type_combo.get_active() == 1:
879                account_type = AccountsService.UserAccountType.ADMINISTRATOR
880            else:
881                account_type = AccountsService.UserAccountType.STANDARD
882            fullname = dialog.realname_entry.get_text()
883            username = dialog.username_entry.get_text()
884            new_user = self.accountService.create_user(username, fullname, account_type)
885            new_user.set_password_mode(AccountsService.UserPasswordMode.NONE)
886            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size("/usr/local/share/cinnamon/faces/user-generic.png", 48, 48)
887            description = "<b>%s</b>\n%s" % (fullname, username)
888            piter = self.users.append(None, [new_user, pixbuf, description])
889            # Add the user to his/her own group and sudo if Administrator was selected
890            if dialog.account_type_combo.get_active() == 1:
891                subprocess.call(["usermod", username, "-G", "%s,sudo,nopasswdlogin" % username])
892            else:
893                subprocess.call(["usermod", username, "-G", "%s,nopasswdlogin" % username])
894            self.load_groups()
895        dialog.destroy()
896
897    def on_user_edition(self, event):
898        model, treeiter = self.users_treeview.get_selection().get_selected()
899        if treeiter != None:
900            print("Editing user %s" % model[treeiter][INDEX_USER_OBJECT].get_user_name())
901
902# GROUPS CALLBACKS
903
904    def on_group_selection(self, selection):
905        model, treeiter = selection.get_selected()
906        if treeiter != None:
907            self.builder.get_object("button_edit_group").set_sensitive(True)
908            self.builder.get_object("button_delete_group").set_sensitive(True)
909            self.builder.get_object("button_delete_group").set_tooltip_text("")
910            group = model[treeiter][INDEX_GROUPNAME]
911            for p in pwd.getpwall():
912                username = p[0]
913                primary_group = grp.getgrgid(p[3])[0]
914                if primary_group == group:
915                    self.builder.get_object("button_delete_group").set_sensitive(False)
916                    self.builder.get_object("button_delete_group").set_tooltip_text(_("This group is set as %s's primary group") % username)
917                    break
918
919        else:
920            self.builder.get_object("button_edit_group").set_sensitive(False)
921            self.builder.get_object("button_delete_group").set_sensitive(False)
922            self.builder.get_object("button_delete_group").set_tooltip_text("")
923
924    def on_group_deletion(self, event):
925        model, treeiter = self.groups_treeview.get_selection().get_selected()
926        if treeiter != None:
927            group = model[treeiter][INDEX_GROUPNAME]
928            message = _("Are you sure you want to permanently delete %s?") % group
929            d = Gtk.MessageDialog(self.window,
930                                  Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
931                                  Gtk.MessageType.QUESTION,
932                                  Gtk.ButtonsType.YES_NO,
933                                  message)
934            d.set_markup(message)
935            d.set_default_response(Gtk.ResponseType.NO)
936            r = d.run()
937            if r == Gtk.ResponseType.YES:
938                subprocess.call(["groupdel", group])
939                self.load_groups()
940            d.destroy()
941
942    def on_group_addition(self, event):
943        dialog = GroupDialog(_("Group Name"), "", self.window)
944        response = dialog.run()
945        if response == Gtk.ResponseType.OK:
946            subprocess.call(["groupadd", dialog.entry.get_text().lower()])
947            self.load_groups()
948        dialog.destroy()
949
950    def on_group_edition(self, event):
951        model, treeiter = self.groups_treeview.get_selection().get_selected()
952        if treeiter != None:
953            group = model[treeiter][INDEX_GROUPNAME]
954            dialog = GroupDialog(_("Group Name"), group, self.window)
955            response = dialog.run()
956            if response == Gtk.ResponseType.OK:
957                subprocess.call(["groupmod", group, "-n", dialog.entry.get_text().lower()])
958                self.load_groups()
959            dialog.destroy()
960
961
962if __name__ == "__main__":
963    setproctitle("cinnamon-settings-users")
964    module = Module()
965    Gtk.main()
966