diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_user.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_user.py new file mode 100644 index 0000000..080d32a --- /dev/null +++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_user.py @@ -0,0 +1,499 @@ +#!/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(path) + self.face_button.set_picture_from_file(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