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