1#!/usr/local/bin/python3.8 2 3try: 4 import pam 5 print("Using pam module (python3-pampy)") 6except: 7 import PAM 8 print("Using PAM module (python3-pam)") 9 pam = None 10import pexpect 11import time 12from random import randint 13import shutil 14import os 15import subprocess 16 17import PIL 18import gi 19gi.require_version('AccountsService', '1.0') 20from gi.repository import AccountsService, GLib, GdkPixbuf 21 22from SettingsWidgets import SidePage 23from ChooserButtonWidgets import PictureChooserButton 24from xapp.GSettingsWidgets import * 25 26class PasswordError(Exception): 27 '''Exception raised when an incorrect password is supplied.''' 28 pass 29 30 31class Module: 32 name = "user" 33 category = "prefs" 34 comment = _("Change your user preferences and password") 35 36 def __init__(self, content_box): 37 keywords = _("user, account, information, details, password") 38 sidePage = SidePage(_("Account details"), "cs-user", keywords, content_box, module=self) 39 self.sidePage = sidePage 40 self.window = None 41 42 def _setParentRef(self, window): 43 self.window = window 44 45 def on_module_selected(self): 46 if not self.loaded: 47 print("Loading User module") 48 49 page = SettingsPage() 50 self.sidePage.add_widget(page) 51 52 settings = page.add_section(_("Account details")) 53 54 self.scale = self.window.get_scale_factor() 55 56 self.face_button = PictureChooserButton(num_cols=4, button_picture_size=64, menu_pictures_size=64*self.scale, keep_square=True) 57 self.face_button.set_alignment(0.0, 0.5) 58 self.face_button.set_tooltip_text(_("Click to change your picture")) 59 60 self.face_photo_menuitem = Gtk.MenuItem.new_with_label(_("Take a photo...")) 61 self.face_photo_menuitem.connect('activate', self._on_face_photo_menuitem_activated) 62 63 self.face_browse_menuitem = Gtk.MenuItem.new_with_label(_("Browse for more pictures...")) 64 self.face_browse_menuitem.connect('activate', self._on_face_browse_menuitem_activated) 65 66 face_dirs = ["/usr/local/share/cinnamon/faces"] 67 for face_dir in face_dirs: 68 if os.path.exists(face_dir): 69 pictures = sorted(os.listdir(face_dir)) 70 for picture in pictures: 71 path = os.path.join(face_dir, picture) 72 self.face_button.add_picture(path, self._on_face_menuitem_activated) 73 74 widget = SettingsWidget() 75 label = Gtk.Label.new(_("Picture")) 76 widget.pack_start(label, False, False, 0) 77 widget.pack_end(self.face_button, False, False, 0) 78 settings.add_row(widget) 79 80 size_group = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL) 81 82 widget = SettingsWidget() 83 label = Gtk.Label.new(_("Name")) 84 widget.pack_start(label, False, False, 0) 85 self.realname_entry = EditableEntry() 86 size_group.add_widget(self.realname_entry) 87 self.realname_entry.connect("changed", self._on_realname_changed) 88 self.realname_entry.set_tooltip_text(_("Click to change your name")) 89 widget.pack_end(self.realname_entry, False, False, 0) 90 settings.add_row(widget) 91 92 widget = SettingsWidget() 93 label = Gtk.Label.new(_("Password")) 94 widget.pack_start(label, False, False, 0) 95 password_mask = Gtk.Label.new('\u2022\u2022\u2022\u2022\u2022\u2022') 96 password_mask.set_alignment(0.9, 0.5) 97 self.password_button = Gtk.Button() 98 size_group.add_widget(self.password_button) 99 self.password_button.add(password_mask) 100 self.password_button.set_relief(Gtk.ReliefStyle.NONE) 101 self.password_button.set_tooltip_text(_("Click to change your password")) 102 self.password_button.connect('activate', self._on_password_button_clicked) 103 self.password_button.connect('released', self._on_password_button_clicked) 104 widget.pack_end(self.password_button, False, False, 0) 105 settings.add_row(widget) 106 107 current_user = GLib.get_user_name() 108 self.accountService = AccountsService.UserManager.get_default().get_user(current_user) 109 self.accountService.connect('notify::is-loaded', self.load_user_info) 110 111 self.face_button.add_separator() 112 113 # Video devices assumed to be webcams 114 import glob 115 webcam_detected = len(glob.glob("/dev/video*")) > 0 116 117 if webcam_detected: 118 self.face_button.add_menuitem(self.face_photo_menuitem) 119 120 self.face_button.add_menuitem(self.face_browse_menuitem) 121 122 def update_preview_cb (self, dialog, preview): 123 filename = dialog.get_preview_filename() 124 if filename is not None: 125 if os.path.isfile(filename): 126 try: 127 pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(filename, 128, 128) 128 if pixbuf is not None: 129 preview.set_from_pixbuf(pixbuf) 130 self.frame.show() 131 return 132 except GLib.Error as e: 133 print("Unable to generate preview for file '%s' - %s\n" % (filename, e.message)) 134 135 preview.clear() 136 self.frame.hide() 137 138 def _on_face_photo_menuitem_activated(self, menuitem): 139 140 # streamer takes -t photos, uses /dev/video0 141 if 0 != subprocess.call(["streamer", "-j90", "-t8", "-s800x600", "-o", "/tmp/temp-account-pic00.jpeg"]): 142 print("Error: Webcam not available") 143 return 144 145 # Use the 8th frame (the webcam takes a few frames to "lighten up") 146 path = "/tmp/temp-account-pic07.jpeg" 147 148 # Crop the image to thumbnail size 149 image = PIL.Image.open(path) 150 width, height = image.size 151 152 if width > height: 153 new_width = height 154 new_height = height 155 elif height > width: 156 new_width = width 157 new_height = width 158 else: 159 new_width = width 160 new_height = height 161 162 left = (width - new_width) / 2 163 top = (height - new_height) / 2 164 right = (width + new_width) / 2 165 bottom = (height + new_height) / 2 166 167 image = image.crop((left, top, right, bottom)) 168 image.thumbnail((255, 255), PIL.Image.ANTIALIAS) 169 170 face_path = os.path.join(self.accountService.get_home_dir(), ".face") 171 172 image.save(face_path, "png") 173 self.accountService.set_icon_file(face_path) 174 self.face_button.set_picture_from_file(face_path) 175 176 177 def _on_face_browse_menuitem_activated(self, menuitem): 178 dialog = Gtk.FileChooserDialog(None, None, Gtk.FileChooserAction.OPEN, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) 179 dialog.set_current_folder(self.accountService.get_home_dir()) 180 filter = Gtk.FileFilter() 181 filter.set_name(_("Images")) 182 filter.add_mime_type("image/*") 183 dialog.add_filter(filter) 184 185 box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 186 self.frame = Gtk.Frame(visible=False, no_show_all=True) 187 preview = Gtk.Image(visible=True) 188 189 box.pack_start(self.frame, False, False, 0) 190 self.frame.add(preview) 191 dialog.set_preview_widget(box) 192 dialog.set_preview_widget_active(True) 193 dialog.set_use_preview_label(False) 194 195 box.set_margin_end(12) 196 box.set_margin_top(12) 197 box.set_size_request(128, -1) 198 199 dialog.connect("update-preview", self.update_preview_cb, preview) 200 201 response = dialog.run() 202 if response == Gtk.ResponseType.OK: 203 path = dialog.get_filename() 204 image = PIL.Image.open(path) 205 image.thumbnail((255, 255), PIL.Image.ANTIALIAS) 206 face_path = os.path.join(self.accountService.get_home_dir(), ".face") 207 image.save(face_path, "png") 208 self.accountService.set_icon_file(face_path) 209 self.face_button.set_picture_from_file(face_path) 210 211 dialog.destroy() 212 213 def _on_face_menuitem_activated(self, path): 214 if os.path.exists(path): 215 self.accountService.set_icon_file(path) 216 shutil.copy(path, os.path.join(self.accountService.get_home_dir(), ".face")) 217 return True 218 219 def load_user_info(self, user, param): 220 self.realname_entry.set_text(user.get_real_name()) 221 for path in [os.path.join(self.accountService.get_home_dir(), ".face"), user.get_icon_file(), "/usr/local/share/cinnamon/faces/user-generic.png"]: 222 if os.path.exists(path): 223 self.face_button.set_picture_from_file(path) 224 break 225 226 def _on_realname_changed(self, widget, text): 227 self.accountService.set_real_name(text) 228 229 def _on_password_button_clicked(self, widget): 230 dialog = PasswordDialog() 231 response = dialog.run() 232 233 234class PasswordDialog(Gtk.Dialog): 235 236 def __init__ (self): 237 super(PasswordDialog, self).__init__() 238 239 self.correct_current_password = False # Flag to remember if the current password is correct or not 240 241 self.set_modal(True) 242 self.set_skip_taskbar_hint(True) 243 self.set_skip_pager_hint(True) 244 self.set_title(_("Change Password")) 245 246 table = Gtk.Table(6, 3) 247 table.set_border_width(6) 248 table.set_row_spacings(8) 249 table.set_col_spacings(15) 250 251 label = Gtk.Label.new(_("Current password")) 252 label.set_alignment(1, 0.5) 253 table.attach(label, 0, 1, 0, 1) 254 255 label = Gtk.Label.new(_("New password")) 256 label.set_alignment(1, 0.5) 257 table.attach(label, 0, 1, 1, 2) 258 259 label = Gtk.Label.new(_("Confirm password")) 260 label.set_alignment(1, 0.5) 261 table.attach(label, 0, 1, 3, 4) 262 263 self.current_password = Gtk.Entry() 264 self.current_password.set_visibility(False) 265 self.current_password.connect("focus-out-event", self._on_current_password_changed) 266 table.attach(self.current_password, 1, 3, 0, 1) 267 268 self.new_password = Gtk.Entry() 269 self.new_password.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "view-refresh") 270 self.new_password.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("Generate a password")) 271 self.new_password.set_tooltip_text(_("Generate a password")) 272 self.new_password.connect("icon-release", self._on_new_password_icon_released) 273 self.new_password.connect("changed", self._on_passwords_changed) 274 table.attach(self.new_password, 1, 3, 1, 2) 275 276 self.strengh_indicator = Gtk.ProgressBar() 277 self.strengh_indicator.set_tooltip_text(_("Your new password needs to be at least 8 characters long")) 278 self.strengh_indicator.set_fraction(0.0) 279 table.attach(self.strengh_indicator, 1, 2, 2, 3, xoptions=Gtk.AttachOptions.EXPAND|Gtk.AttachOptions.FILL) 280 self.strengh_indicator.set_size_request(-1, 1) 281 282 self.strengh_label = Gtk.Label() 283 self.strengh_label.set_tooltip_text(_("Your new password needs to be at least 8 characters long")) 284 self.strengh_label.set_alignment(1, 0.5) 285 table.attach(self.strengh_label, 2, 3, 2, 3) 286 287 self.confirm_password = Gtk.Entry() 288 self.confirm_password.connect("changed", self._on_passwords_changed) 289 table.attach(self.confirm_password, 1, 3, 3, 4) 290 291 self.show_password = Gtk.CheckButton(_("Show password")) 292 self.show_password.connect('toggled', self._on_show_password_toggled) 293 table.attach(self.show_password, 1, 3, 4, 5) 294 295 self.set_border_width(6) 296 297 box = self.get_content_area() 298 box.add(table) 299 self.show_all() 300 301 self.infobar = Gtk.InfoBar() 302 self.infobar.set_message_type(Gtk.MessageType.ERROR) 303 label = Gtk.Label.new(_("An error occurred. Your password was not changed.")) 304 content = self.infobar.get_content_area() 305 content.add(label) 306 table.attach(self.infobar, 0, 3, 5, 6) 307 308 self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, _("Change"), Gtk.ResponseType.OK, ) 309 310 self.set_passwords_visibility() 311 self.set_response_sensitive(Gtk.ResponseType.OK, False) 312 self.infobar.hide() 313 314 self.connect("response", self._on_response) 315 316 def _on_response(self, dialog, response_id): 317 if response_id == Gtk.ResponseType.OK: 318 self.change_password() 319 else: 320 self.destroy() 321 322 def change_password(self): 323 oldpass = self.current_password.get_text() 324 newpass = self.new_password.get_text() 325 passwd = pexpect.spawn("/usr/local/bin/passwd") 326 time.sleep(0.5) 327 passwd.sendline(oldpass) 328 time.sleep(0.5) 329 passwd.sendline(newpass) 330 time.sleep(0.5) 331 passwd.sendline(newpass) 332 time.sleep(0.5) 333 passwd.close() 334 335 if passwd.exitstatus is None or passwd.exitstatus > 0: 336 self.infobar.show_all() 337 else: 338 self.destroy() 339 340 def set_passwords_visibility(self): 341 visible = self.show_password.get_active() 342 self.new_password.set_visibility(visible) 343 self.confirm_password.set_visibility(visible) 344 345 def _on_new_password_icon_released(self, widget, icon_pos, event): 346 self.infobar.hide() 347 self.show_password.set_active(True) 348 characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-" 349 newpass = "" 350 for i in range (8): 351 index = randint(0, len(characters) -1) 352 newpass = newpass + characters[index] 353 354 self.new_password.set_text(newpass) 355 self.confirm_password.set_text(newpass) 356 self.check_passwords() 357 358 def _on_show_password_toggled(self, widget): 359 self.set_passwords_visibility() 360 361 def auth_pam(self): 362 if not pam.pam().authenticate(GLib.get_user_name(), self.current_password.get_text(), 'passwd'): 363 raise PasswordError("Invalid password") 364 365 def auth_PyPAM(self): 366 auth = PAM.pam() 367 auth.start('passwd') 368 auth.set_item(PAM.PAM_USER, GLib.get_user_name()) 369 auth.set_item(PAM.PAM_CONV, self.pam_conv) 370 try: 371 auth.authenticate() 372 auth.acct_mgmt() 373 return True 374 except PAM.error as resp: 375 raise PasswordError("Invalid password") 376 377 def _on_current_password_changed(self, widget, event): 378 self.infobar.hide() 379 if self.current_password.get_text() != "": 380 try: 381 self.auth_pam() if pam else self.auth_PyPAM() 382 except PasswordError: 383 self.current_password.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, Gtk.STOCK_DIALOG_WARNING) 384 self.current_password.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("Wrong password")) 385 self.current_password.set_tooltip_text(_("Wrong password")) 386 self.correct_current_password = False 387 except: 388 self.current_password.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, Gtk.STOCK_DIALOG_WARNING) 389 self.current_password.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("Internal Error")) 390 self.current_password.set_tooltip_text(_("Internal Error")) 391 self.correct_current_password = False 392 raise 393 else: 394 self.current_password.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None) 395 self.current_password.set_tooltip_text("") 396 self.correct_current_password = True 397 self.check_passwords() 398 399 # Based on setPasswordStrength() in Mozilla Seamonkey, which is tri-licensed under MPL 1.1, GPL 2.0, and LGPL 2.1. 400 # Forked from Ubiquity validation.py 401 def password_strength(self, password): 402 upper = lower = digit = symbol = 0 403 for char in password: 404 if char.isdigit(): 405 digit += 1 406 elif char.islower(): 407 lower += 1 408 elif char.isupper(): 409 upper += 1 410 else: 411 symbol += 1 412 length = len(password) 413 414 length = min(length,4) 415 digit = min(digit,3) 416 upper = min(upper,3) 417 symbol = min(symbol,3) 418 strength = ( 419 ((length * 0.1) - 0.2) + 420 (digit * 0.1) + 421 (symbol * 0.15) + 422 (upper * 0.1)) 423 if strength > 1: 424 strength = 1 425 if strength < 0: 426 strength = 0 427 return strength 428 429 def _on_passwords_changed(self, widget): 430 self.infobar.hide() 431 new_password = self.new_password.get_text() 432 confirm_password = self.confirm_password.get_text() 433 strength = self.password_strength(new_password) 434 if new_password != confirm_password: 435 self.confirm_password.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, Gtk.STOCK_DIALOG_WARNING) 436 self.confirm_password.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("Passwords do not match")) 437 self.confirm_password.set_tooltip_text(_("Passwords do not match")) 438 else: 439 self.confirm_password.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None) 440 self.confirm_password.set_tooltip_text("") 441 if len(new_password) < 8: 442 self.strengh_label.set_text(_("Too short")) 443 self.strengh_indicator.set_fraction(0.0) 444 elif strength < 0.6: 445 self.strengh_label.set_text(_("Weak")) 446 self.strengh_indicator.set_fraction(0.2) 447 elif strength < 0.75: 448 self.strengh_label.set_text(_("Fair")) 449 self.strengh_indicator.set_fraction(0.4) 450 elif strength < 0.9: 451 self.strengh_label.set_text(_("Good")) 452 self.strengh_indicator.set_fraction(0.6) 453 else: 454 self.strengh_label.set_text(_("Strong")) 455 self.strengh_indicator.set_fraction(1.0) 456 457 self.check_passwords() 458 459 def check_passwords(self): 460 if self.correct_current_password: 461 new_password = self.new_password.get_text() 462 confirm_password = self.confirm_password.get_text() 463 if len(new_password) >= 8 and new_password == confirm_password: 464 self.set_response_sensitive(Gtk.ResponseType.OK, True) 465 else: 466 self.set_response_sensitive(Gtk.ResponseType.OK, False) 467 468 def pam_conv(self, auth, query_list, userData): 469 resp = [] 470 for i in range(len(query_list)): 471 query, type = query_list[i] 472 val = self.current_password.get_text() 473 resp.append((val, 0)) 474 return resp 475