#!/usr/bin/python3 try: import pam print("Using pam module (python3-pampy)") except: import PAM print("Using PAM module (python3-pam)") pam = None import pexpect import time from random import randint import shutil import os import subprocess from PIL import Image import gi gi.require_version('AccountsService', '1.0') from gi.repository import AccountsService, GLib, GdkPixbuf, XApp from bin.SettingsWidgets import SidePage from bin.ChooserButtonWidgets import PictureChooserButton from xapp.GSettingsWidgets import * class PasswordError(Exception): """Exception raised when an incorrect password is supplied.""" pass class Module: name = "user" category = "prefs" comment = _("Change your user preferences and password") def __init__(self, content_box): keywords = _("user, account, information, details, password") sidePage = SidePage(_("Account Details"), "cs-user", keywords, content_box, module=self) self.sidePage = sidePage self.window = None def _setParentRef(self, window): self.window = window def on_module_selected(self): if not self.loaded: print("Loading User module") page = SettingsPage() self.sidePage.add_widget(page) settings = page.add_section(_("Account Details")) self.scale = self.window.get_scale_factor() self.face_button = PictureChooserButton(num_cols=4, button_picture_width=64, menu_picture_width=64*self.scale, keep_square=True) self.face_button.set_alignment(0.0, 0.5) self.face_button.set_tooltip_text(_("Click to change your picture")) self.face_photo_menuitem = Gtk.MenuItem.new_with_label(_("Take a photo...")) self.face_photo_menuitem.connect('activate', self._on_face_photo_menuitem_activated) self.face_browse_menuitem = Gtk.MenuItem.new_with_label(_("Browse for more pictures...")) self.face_browse_menuitem.connect('activate', self._on_face_browse_menuitem_activated) face_dirs = ["/usr/share/cinnamon/faces"] for face_dir in face_dirs: if os.path.exists(face_dir): pictures = sorted(os.listdir(face_dir)) for picture in pictures: path = os.path.join(face_dir, picture) self.face_button.add_picture(path, self._on_face_menuitem_activated) widget = SettingsWidget() label = Gtk.Label.new(_("Picture")) widget.pack_start(label, False, False, 0) widget.pack_end(self.face_button, False, False, 0) settings.add_row(widget) size_group = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL) widget = SettingsWidget() label = Gtk.Label.new(_("Name")) widget.pack_start(label, False, False, 0) self.realname_entry = EditableEntry() size_group.add_widget(self.realname_entry) self.realname_entry.connect("changed", self._on_realname_changed) self.realname_entry.set_tooltip_text(_("Click to change your name")) widget.pack_end(self.realname_entry, False, False, 0) settings.add_row(widget) widget = SettingsWidget() label = Gtk.Label.new(_("Password")) widget.pack_start(label, False, False, 0) password_mask = Gtk.Label.new('\u2022\u2022\u2022\u2022\u2022\u2022') password_mask.set_alignment(0.9, 0.5) self.password_button = Gtk.Button() size_group.add_widget(self.password_button) self.password_button.add(password_mask) self.password_button.set_relief(Gtk.ReliefStyle.NONE) self.password_button.set_tooltip_text(_("Click to change your password")) self.password_button.connect('activate', self._on_password_button_clicked) self.password_button.connect('released', self._on_password_button_clicked) widget.pack_end(self.password_button, False, False, 0) settings.add_row(widget) current_user = GLib.get_user_name() self.accountService = AccountsService.UserManager.get_default().get_user(current_user) self.accountService.connect('notify::is-loaded', self.load_user_info) self.face_button.add_separator() # Video devices assumed to be webcams import glob webcam_detected = len(glob.glob("/dev/video*")) > 0 if webcam_detected: self.face_button.add_menuitem(self.face_photo_menuitem) self.face_button.add_menuitem(self.face_browse_menuitem) def update_preview_cb (self, dialog, preview): filename = dialog.get_preview_filename() if filename is not None: if os.path.isfile(filename): try: pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(filename, 128, 128) if pixbuf is not None: preview.set_from_pixbuf(pixbuf) self.frame.show() return except GLib.Error as e: print(f"Unable to generate preview for file '{filename}' - {e.message}\n") preview.clear() self.frame.hide() def _on_face_photo_menuitem_activated(self, menuitem): # streamer takes -t photos, uses /dev/video0 if 0 != subprocess.call(["streamer", "-j90", "-t8", "-s800x600", "-o", "/tmp/temp-account-pic00.jpeg"]): print("Error: Webcam not available") return # Use the 8th frame (the webcam takes a few frames to "lighten up") path = "/tmp/temp-account-pic07.jpeg" # Crop the image to thumbnail size image = Image.open(path) width, height = image.size if width > height: new_width = height new_height = height elif height > width: new_width = width new_height = width else: new_width = width new_height = height left = (width - new_width) / 2 top = (height - new_height) / 2 right = (width + new_width) / 2 bottom = (height + new_height) / 2 image = image.crop((left, top, right, bottom)) image.thumbnail((255, 255), Image.LANCZOS) face_path = os.path.join(self.accountService.get_home_dir(), ".face") image.save(face_path, "png") self.accountService.set_icon_file(face_path) self.face_button.set_picture_from_file(face_path) def _on_face_browse_menuitem_activated(self, menuitem): dialog = Gtk.FileChooserDialog(None, None, Gtk.FileChooserAction.OPEN, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) dialog.set_current_folder(self.accountService.get_home_dir()) filter = Gtk.FileFilter() filter.set_name(_("Images")) filter.add_mime_type("image/*") dialog.add_filter(filter) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.frame = Gtk.Frame(visible=False, no_show_all=True) preview = Gtk.Image(visible=True) box.pack_start(self.frame, False, False, 0) self.frame.add(preview) dialog.set_preview_widget(box) dialog.set_preview_widget_active(True) dialog.set_use_preview_label(False) box.set_margin_end(12) box.set_margin_top(12) box.set_size_request(128, -1) dialog.connect("update-preview", self.update_preview_cb, preview) response = dialog.run() if response == Gtk.ResponseType.OK: string = dialog.get_filename() print(string) if string.startswith("/"): path = string else: theme = Gtk.IconTheme.get_default() icon_info = theme.lookup_icon_for_scale(string, 256, dialog.get_scale_factor(), Gtk.IconLookupFlags.FORCE_SIZE) path = icon_info.get_filename() if icon_info else None face_path = os.path.join(self.accountService.get_home_dir(), ".face") pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(path, 255, -1) pixbuf.savev(face_path, "png") self.accountService.set_icon_file(face_path) self.face_button.set_picture_from_file(face_path) dialog.destroy() def _on_face_menuitem_activated(self, path): if os.path.exists(path): self.accountService.set_icon_file(path) shutil.copy(path, os.path.join(self.accountService.get_home_dir(), ".face")) return True def load_user_info(self, user, param): self.realname_entry.set_text(user.get_real_name()) for path in [os.path.join(self.accountService.get_home_dir(), ".face"), user.get_icon_file(), "/usr/share/cinnamon/faces/user-generic.png"]: if os.path.exists(path): self.face_button.set_picture_from_file(path) break def _on_realname_changed(self, widget, text): self.accountService.set_real_name(text) def _on_password_button_clicked(self, widget): dialog = PasswordDialog() response = dialog.run() class PasswordDialog(Gtk.Dialog): def __init__ (self): super(PasswordDialog, self).__init__() self.correct_current_password = False # Flag to remember if the current password is correct or not self.set_modal(True) self.set_skip_taskbar_hint(True) self.set_skip_pager_hint(True) self.set_title(_("Change Password")) table = Gtk.Table(6, 3) table.set_border_width(6) table.set_row_spacings(8) table.set_col_spacings(15) label = Gtk.Label.new(_("Current password")) label.set_alignment(1, 0.5) table.attach(label, 0, 1, 0, 1) label = Gtk.Label.new(_("New password")) label.set_alignment(1, 0.5) table.attach(label, 0, 1, 1, 2) label = Gtk.Label.new(_("Confirm password")) label.set_alignment(1, 0.5) table.attach(label, 0, 1, 3, 4) self.current_password = Gtk.Entry() self.current_password.set_visibility(False) self.current_password.connect("focus-out-event", self._on_current_password_changed) table.attach(self.current_password, 1, 3, 0, 1) self.new_password = Gtk.Entry() self.new_password.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "view-refresh") self.new_password.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("Generate a password")) self.new_password.set_tooltip_text(_("Generate a password")) self.new_password.connect("icon-release", self._on_new_password_icon_released) self.new_password.connect("changed", self._on_passwords_changed) table.attach(self.new_password, 1, 3, 1, 2) self.strengh_indicator = Gtk.ProgressBar() self.strengh_indicator.set_tooltip_text(_("Your new password needs to be at least 8 characters long")) self.strengh_indicator.set_fraction(0.0) table.attach(self.strengh_indicator, 1, 2, 2, 3, xoptions=Gtk.AttachOptions.EXPAND|Gtk.AttachOptions.FILL) self.strengh_indicator.set_size_request(-1, 1) self.strengh_label = Gtk.Label() self.strengh_label.set_tooltip_text(_("Your new password needs to be at least 8 characters long")) self.strengh_label.set_alignment(1, 0.5) table.attach(self.strengh_label, 2, 3, 2, 3) self.confirm_password = Gtk.Entry() self.confirm_password.connect("changed", self._on_passwords_changed) table.attach(self.confirm_password, 1, 3, 3, 4) self.show_password = Gtk.CheckButton(_("Show password")) self.show_password.connect('toggled', self._on_show_password_toggled) table.attach(self.show_password, 1, 3, 4, 5) self.set_border_width(6) box = self.get_content_area() box.add(table) self.show_all() self.infobar = Gtk.InfoBar() self.infobar.set_message_type(Gtk.MessageType.ERROR) label = Gtk.Label.new(_("An error occurred. Your password was not changed.")) content = self.infobar.get_content_area() content.add(label) table.attach(self.infobar, 0, 3, 5, 6) self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, _("Change"), Gtk.ResponseType.OK, ) self.set_passwords_visibility() self.set_response_sensitive(Gtk.ResponseType.OK, False) self.infobar.hide() self.connect("response", self._on_response) def _on_response(self, dialog, response_id): if response_id == Gtk.ResponseType.OK: self.change_password() else: self.destroy() def change_password(self): oldpass = self.current_password.get_text() newpass = self.new_password.get_text() passwd = pexpect.spawn("/usr/bin/passwd") # passwd only asks for the old password when there already is one set. if oldpass == "": time.sleep(0.5) passwd.sendline(newpass) time.sleep(0.5) passwd.sendline(newpass) else: time.sleep(0.5) passwd.sendline(oldpass) time.sleep(0.5) passwd.sendline(newpass) time.sleep(0.5) passwd.sendline(newpass) time.sleep(0.5) passwd.close() if passwd.exitstatus is None or passwd.exitstatus > 0: self.infobar.show_all() else: self.destroy() def set_passwords_visibility(self): visible = self.show_password.get_active() self.new_password.set_visibility(visible) self.confirm_password.set_visibility(visible) def _on_new_password_icon_released(self, widget, icon_pos, event): self.infobar.hide() self.show_password.set_active(True) characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-" newpass = "" for i in range (8): index = randint(0, len(characters) -1) newpass = newpass + characters[index] self.new_password.set_text(newpass) self.confirm_password.set_text(newpass) self.check_passwords() def _on_show_password_toggled(self, widget): self.set_passwords_visibility() def _get_pam_service(self): import os if os.path.exists('/etc/pam.d/system-auth'): return 'system-auth' elif os.path.exists('/etc/pam.d/common-auth'): return 'common-auth' else: return 'login' def auth_pam(self): service = self._get_pam_service() if not pam.pam().authenticate(GLib.get_user_name(), self.current_password.get_text(), service): raise PasswordError("Invalid password") def auth_PyPAM(self): auth = PAM.pam() auth.start(self._get_pam_service()) auth.set_item(PAM.PAM_USER, GLib.get_user_name()) auth.set_item(PAM.PAM_CONV, self.pam_conv) try: auth.authenticate() auth.acct_mgmt() return True except PAM.error as resp: raise PasswordError("Invalid password") def _on_current_password_changed(self, widget, event): self.infobar.hide() try: self.auth_pam() if pam else self.auth_PyPAM() except PasswordError: self.current_password.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, Gtk.STOCK_DIALOG_WARNING) self.current_password.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("Wrong password")) self.current_password.set_tooltip_text(_("Wrong password")) self.correct_current_password = False except: self.current_password.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, Gtk.STOCK_DIALOG_WARNING) self.current_password.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("Internal Error")) self.current_password.set_tooltip_text(_("Internal Error")) self.correct_current_password = False raise else: self.current_password.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None) self.current_password.set_tooltip_text("") self.correct_current_password = True self.check_passwords() # Based on setPasswordStrength() in Mozilla Seamonkey, which is tri-licensed under MPL 1.1, GPL 2.0, and LGPL 2.1. # Forked from Ubiquity validation.py def password_strength(self, password): upper = lower = digit = symbol = 0 for char in password: if char.isdigit(): digit += 1 elif char.islower(): lower += 1 elif char.isupper(): upper += 1 else: symbol += 1 length = len(password) length = min(length,4) digit = min(digit,3) upper = min(upper,3) symbol = min(symbol,3) strength = ( ((length * 0.1) - 0.2) + (digit * 0.1) + (symbol * 0.15) + (upper * 0.1)) if strength > 1: strength = 1 if strength < 0: strength = 0 return strength def _on_passwords_changed(self, widget): self.infobar.hide() new_password = self.new_password.get_text() confirm_password = self.confirm_password.get_text() strength = self.password_strength(new_password) if new_password != confirm_password: self.confirm_password.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, Gtk.STOCK_DIALOG_WARNING) self.confirm_password.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _("Passwords do not match")) self.confirm_password.set_tooltip_text(_("Passwords do not match")) else: self.confirm_password.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None) self.confirm_password.set_tooltip_text("") if len(new_password) < 8: self.strengh_label.set_text(_("Too short")) self.strengh_indicator.set_fraction(0.0) elif strength < 0.6: self.strengh_label.set_text(_("Weak")) self.strengh_indicator.set_fraction(0.2) elif strength < 0.75: self.strengh_label.set_text(_("Fair")) self.strengh_indicator.set_fraction(0.4) elif strength < 0.9: self.strengh_label.set_text(_("Good")) self.strengh_indicator.set_fraction(0.6) else: self.strengh_label.set_text(_("Strong")) self.strengh_indicator.set_fraction(1.0) self.check_passwords() def check_passwords(self): if self.correct_current_password: new_password = self.new_password.get_text() confirm_password = self.confirm_password.get_text() if len(new_password) >= 8 and new_password == confirm_password: self.set_response_sensitive(Gtk.ResponseType.OK, True) else: self.set_response_sensitive(Gtk.ResponseType.OK, False) def pam_conv(self, auth, query_list, userData): resp = [] for i in range(len(query_list)): query, type = query_list[i] val = self.current_password.get_text() resp.append((val, 0)) return resp