From 67513c2357b2c1d0ce91c9a73aef3e77886a2b70 Mon Sep 17 00:00:00 2001 From: Sean Davis Date: Sun, 24 Aug 2014 11:46:34 -0400 Subject: [PATCH] Use the new SudoDialog --- data/ui/MugshotWindow.ui | 4 +- mugshot/MugshotWindow.py | 32 +++-- mugshot_lib/SudoDialog.py | 245 ++++++++++++++++++++++++-------------- po/mugshot.pot | 60 ++++++---- 4 files changed, 215 insertions(+), 126 deletions(-) diff --git a/data/ui/MugshotWindow.ui b/data/ui/MugshotWindow.ui index c81102d..ed49cba 100644 --- a/data/ui/MugshotWindow.ui +++ b/data/ui/MugshotWindow.ui @@ -1,8 +1,8 @@ - + - + True diff --git a/mugshot/MugshotWindow.py b/mugshot/MugshotWindow.py index 4c6a1fe..e4c9fff 100644 --- a/mugshot/MugshotWindow.py +++ b/mugshot/MugshotWindow.py @@ -192,7 +192,7 @@ class MugshotWindow(Window): self.fax_entry = builder.get_object('fax') # Users without sudo rights cannot change their name. - self.set_name_editable(SudoDialog.check_sudo()) + self.set_name_editable(SudoDialog.check_dependencies(['chfn'])) # Stock photo browser self.stock_browser = builder.get_object('stock_browser') @@ -311,13 +311,24 @@ class MugshotWindow(Window): changes.""" logger.debug('Applying changes...') if self.get_chfn_details_updated(): - if not self.save_chfn_details(): + success, response = self.save_chfn_details() + if not success: # Password was incorrect, complain. - primary = _("Authentication Failed") + if response in [Gtk.ResponseType.NONE, + Gtk.ResponseType.CANCEL, + Gtk.ResponseType.DELETE_EVENT]: + msg_type = Gtk.MessageType.WARNING + primary = _("Authentication cancelled.") + elif response == Gtk.ResponseType.REJECT: + msg_type = Gtk.MessageType.WARNING + primary = _("Authentication failed.") + else: + msg_type = Gtk.MessageType.ERROR + primary = _("An error occurred when saving changes.") + secondary = _("User details were not updated.") dialog = Gtk.MessageDialog(transient_for=self, flags=0, - message_type= - Gtk.MessageType.WARNING, + message_type=msg_type, buttons=Gtk.ButtonsType.OK, text=primary) dialog.format_secondary_text(secondary) @@ -555,8 +566,7 @@ class MugshotWindow(Window): """Handle password prompts from the interactive chfn commands.""" # Force the C language for guaranteed english strings in the script. logger.debug('Executing: %s' % command) - child = pexpect.spawn(command, env={"LANG": "C"}) - child.timeout = 5 + child = SudoDialog.env_spawn(command, 5) child.write_to_stdout = True try: child.expect([".*ssword.*", pexpect.EOF]) @@ -582,13 +592,13 @@ class MugshotWindow(Window): sudo_dialog.format_secondary_text(_("This is a security measure to " "prevent unwanted updates\n" "to your personal information.")) - sudo_dialog.run() + response = sudo_dialog.run() sudo_dialog.hide() password = sudo_dialog.get_password() sudo_dialog.destroy() if not password: - return False + return (False, response) sudo = which('sudo') chfn = which('chfn') @@ -606,7 +616,7 @@ class MugshotWindow(Window): home_phone = 'none' # Full name can only be modified by root. Try using sudo to modify. - if SudoDialog.check_sudo(): + if SudoDialog.check_dependencies(['chfn']): logger.debug('Updating Full Name...') command = "%s %s -f \"%s\" %s" % (sudo, chfn, full_name, username) if self.process_terminal_password(command, password): @@ -629,7 +639,7 @@ class MugshotWindow(Window): else: success = False - return success + return (success, response) # = LibreOffice ========================================================= # def get_libreoffice_details_updated(self): diff --git a/mugshot_lib/SudoDialog.py b/mugshot_lib/SudoDialog.py index bb83867..51b3f53 100644 --- a/mugshot_lib/SudoDialog.py +++ b/mugshot_lib/SudoDialog.py @@ -5,7 +5,7 @@ # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or +# the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but @@ -23,22 +23,70 @@ from locale import gettext as _ import pexpect +gtk_version = (Gtk.get_major_version(), + Gtk.get_minor_version(), + Gtk.get_micro_version()) -def check_sudo(): - """Return True if user has permission to use sudo.""" - child = pexpect.spawn('sudo -v', env={"LANG": "C"}) - child.timeout = 1 - # Check for failure message. - try: - child.expect(["Sorry", pexpect.EOF]) - child.close() + +def check_gtk_version(major_version, minor_version, micro=0): + """Return true if running gtk >= requested version""" + return gtk_version >= (major_version, minor_version, micro) + +# Check if the LANG variable needs to be set +use_env = False + + +def check_dependencies(commands=[]): + """Check for the existence of required commands, and sudo access""" + # Check for sudo + if pexpect.which("sudo") is None: return False - except: + + # Check for required commands + for command in commands: + if pexpect.which(command) is None: + return False + + # Check for LANG requirements + child = env_spawn('sudo -v', 1) + if child.expect([".*ssword.*", "Sorry", + pexpect.EOF, + pexpect.TIMEOUT]) == 3: + global use_env + use_env = True + child.close() + + # Check for sudo rights + child = env_spawn('sudo -v', 1) + try: + index = child.expect([".*ssword.*", "Sorry", + pexpect.EOF, pexpect.TIMEOUT]) child.close() - return True + if index == 0 or index == 2: + # User in sudoers, or already admin + return True + elif index == 1 or index == 3: + # User not in sudoers + return False + + except: + # Something else went wrong. + child.close() + + return False -class SudoDialog(Gtk.MessageDialog): +def env_spawn(command, timeout): + """Use pexpect.spawn, adapt for timeout and env requirements.""" + if use_env: + child = pexpect.spawn(command, env={"LANG": "C"}) + else: + child = pexpect.spawn(command) + child.timeout = timeout + return child + + +class SudoDialog(Gtk.Dialog): ''' Creates a new SudoDialog. This is a replacement for using gksudo which provides additional flexibility when performing sudo commands. @@ -58,33 +106,93 @@ class SudoDialog(Gtk.MessageDialog): - REJECT: Password invalid. - ACCEPT: Password valid. ''' - def __init__(self, parent=None, icon=None, message=None, name=None, - retries=-1): + def __init__(self, title=None, parent=None, icon=None, message=None, + name=None, retries=-1): """Initialize the SudoDialog.""" - # default dialog parameters - message_type = Gtk.MessageType.QUESTION - buttons = Gtk.ButtonsType.NONE - # initialize the dialog - super(SudoDialog, self).__init__(transient_for=parent, + super(SudoDialog, self).__init__(title=title, + transient_for=parent, modal=True, - destroy_with_parent=True, - message_type=message_type, - buttons=buttons, - text='') - self.set_dialog_icon(icon) + destroy_with_parent=True) + # self.connect("show", self.on_show) + if title is None: + title = _("Password Required") + self.set_title(title) - # add buttons - button_box = self.get_children()[0].get_children()[1] - self.add_button(_("Cancel"), Gtk.ResponseType.CANCEL) + self.set_border_width(5) + + # Content Area + content_area = self.get_content_area() + grid = Gtk.Grid.new() + grid.set_row_spacing(6) + grid.set_column_spacing(12) + grid.set_margin_left(5) + grid.set_margin_right(5) + content_area.add(grid) + + # Icon + self.dialog_icon = Gtk.Image.new_from_icon_name("dialog-password", + Gtk.IconSize.DIALOG) + grid.attach(self.dialog_icon, 0, 0, 1, 2) + + # Text + self.primary_text = Gtk.Label.new("") + self.primary_text.set_use_markup(True) + self.primary_text.set_halign(Gtk.Align.START) + self.secondary_text = Gtk.Label.new("") + self.secondary_text.set_use_markup(True) + self.secondary_text.set_halign(Gtk.Align.START) + self.secondary_text.set_margin_top(6) + grid.attach(self.primary_text, 1, 0, 1, 1) + grid.attach(self.secondary_text, 1, 1, 1, 1) + + # Infobar + self.infobar = Gtk.InfoBar.new() + self.infobar.set_margin_top(12) + self.infobar.set_message_type(Gtk.MessageType.WARNING) + content_area = self.infobar.get_content_area() + infobar_icon = Gtk.Image.new_from_icon_name("dialog-warning", + Gtk.IconSize.BUTTON) + label = Gtk.Label.new(_("Incorrect password... try again.")) + content_area.add(infobar_icon) + content_area.add(label) + grid.attach(self.infobar, 0, 2, 2, 1) + content_area.show_all() + self.infobar.set_no_show_all(True) + + # Password + label = Gtk.Label.new("") + label.set_use_markup(True) + label.set_markup("%s" % _("Password:")) + label.set_halign(Gtk.Align.START) + label.set_margin_top(12) + self.password_entry = Gtk.Entry() + self.password_entry.set_visibility(False) + self.password_entry.set_activates_default(True) + self.password_entry.set_margin_top(12) + grid.attach(label, 0, 3, 1, 1) + grid.attach(self.password_entry, 1, 3, 1, 1) + + # Buttons + button = self.add_button(_("Cancel"), Gtk.ResponseType.CANCEL) + button_box = button.get_parent() + button_box.set_margin_top(24) ok_button = Gtk.Button.new_with_label(_("OK")) ok_button.connect("clicked", self.on_ok_clicked) ok_button.set_receives_default(True) ok_button.set_can_default(True) ok_button.set_sensitive(False) self.set_default(ok_button) - button_box.pack_start(ok_button, False, False, 0) + if check_gtk_version(3, 12): + button_box.pack_start(ok_button, True, True, 0) + else: + button_box.pack_start(ok_button, False, False, 0) + + self.password_entry.connect("changed", self.on_password_changed, + ok_button) + + self.set_dialog_icon(icon) # add primary and secondary text if message: @@ -98,47 +206,11 @@ class SudoDialog(Gtk.MessageDialog): self.format_primary_text(primary_text) self.format_secondary_text(secondary_text) - # Pack the content area with password-related widgets. - content_area = self.get_content_area() - - # Use an alignment to move align the password widgets with the text. - self.password_alignment = Gtk.Alignment() - # Make an educated guess about how for to align. - left_align = Gtk.icon_size_lookup(Gtk.IconSize.DIALOG)[1] + 16 - self.password_alignment.set_padding(12, 12, left_align, 0) - - # Outer password box for incorrect password label and inner widgets. - password_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, - spacing=12) - password_outer.set_orientation(Gtk.Orientation.VERTICAL) - # Password error label, only displayed when unsuccessful. - self.password_info = Gtk.Label(label="") - self.password_info.set_markup("%s" % - _("Incorrect password... try again.")) - - # Inner password box for Password: label and password entry. - password_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, - spacing=12) - password_label = Gtk.Label(label=_("Password:")) - self.password_entry = Gtk.Entry() - self.password_entry.set_visibility(False) - self.password_entry.set_activates_default(True) - self.password_entry.connect("changed", self.on_password_changed, - ok_button) - - # Pack all the widgets. - password_box.pack_start(password_label, False, False, 0) - password_box.pack_start(self.password_entry, True, True, 0) - password_outer.pack_start(self.password_info, True, True, 0) - password_outer.pack_start(password_box, True, True, 0) - self.password_alignment.add(password_outer) - content_area.pack_start(self.password_alignment, True, True, 0) - content_area.show_all() - self.password_info.set_visible(False) - self.attempted_logins = 0 self.max_attempted_logins = retries + self.show_all() + def on_password_changed(self, widget, button): """Set the apply button sensitivity based on password input.""" button.set_sensitive(len(widget.get_text()) > 0) @@ -146,11 +218,14 @@ class SudoDialog(Gtk.MessageDialog): def format_primary_text(self, message_format): ''' Format the primary text widget. - - API extension to match with format_secondary_text. ''' - label = self.get_message_area().get_children()[0] - label.set_text(message_format) + self.primary_text.set_markup("%s" % message_format) + + def format_secondary_text(self, message_format): + ''' + Format the secondary text widget. + ''' + self.secondary_text.set_markup(message_format) def set_dialog_icon(self, icon=None): ''' @@ -166,24 +241,21 @@ class SudoDialog(Gtk.MessageDialog): pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(icon, icon_size, icon_size) - image = Gtk.Image.new_from_pixbuf(pixbuf) + self.dialog_icon.set_from_pixbuf(pixbuf) self.set_icon_from_file(icon) else: # icon is an named icon, so load it directly to an image - image = Gtk.Image.new_from_icon_name(icon, icon_size) + self.dialog_icon.set_from_icon_name(icon, icon_size) self.set_icon_name(icon) else: # fallback on password icon - image = Gtk.Image.new_from_icon_name('dialog-password', icon_size) + self.dialog_icon.set_from_icon_name('dialog-password', icon_size) self.set_icon_name('dialog-password') - # align, show, and set the image. - image.set_alignment(Gtk.Align.CENTER, Gtk.Align.FILL) - image.show() - self.set_image(image) def on_show(self, widget): '''When the dialog is displayed, clear the password.''' self.set_password('') + self.password_valid = False def on_ok_clicked(self, widget): ''' @@ -193,26 +265,22 @@ class SudoDialog(Gtk.MessageDialog): If unsuccessful, try again until reaching maximum attempted logins, then emit the response signal with REJECT. ''' - top, bottom, left, right = self.password_alignment.get_padding() - # Password cannot be validated without sudo - if (not check_sudo()) or self.attempt_login(): + if self.attempt_login(): self.password_valid = True - # Adjust the dialog for attactiveness. - self.password_alignment.set_padding(12, bottom, left, right) - self.password_info.hide() self.emit("response", Gtk.ResponseType.ACCEPT) else: self.password_valid = False # Adjust the dialog for attactiveness. - self.password_alignment.set_padding(0, bottom, left, right) - self.password_info.show() - self.set_password('') + self.infobar.show() + self.password_entry.grab_focus() if self.attempted_logins == self.max_attempted_logins: self.attempted_logins = 0 self.emit("response", Gtk.ResponseType.REJECT) def get_password(self): '''Return the currently entered password, or None if blank.''' + if not self.password_valid: + return None password = self.password_entry.get_text() if password == '': return None @@ -232,12 +300,11 @@ class SudoDialog(Gtk.MessageDialog): Return True if successful. ''' # Set the pexpect variables and spawn the process. - child = pexpect.spawn('sudo /bin/true', env={"LANG": "C"}) - child.timeout = 1 + child = env_spawn('sudo /bin/true', 1) try: # Check for password prompt or program exit. child.expect([".*ssword.*", pexpect.EOF]) - child.sendline(self.get_password()) + child.sendline(self.password_entry.get_text()) child.expect(pexpect.EOF) except pexpect.TIMEOUT: # If we timeout, that means the password was unsuccessful. diff --git a/po/mugshot.pot b/po/mugshot.pot index a628600..16cc019 100644 --- a/po/mugshot.pot +++ b/po/mugshot.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-08-24 10:25-0400\n" +"POT-Creation-Date: 2014-08-24 11:44-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -61,7 +61,7 @@ msgstr "" msgid "Browse…" msgstr "" -#: ../data/ui/MugshotWindow.ui.h:9 ../mugshot/MugshotWindow.py:579 +#: ../data/ui/MugshotWindow.ui.h:9 ../mugshot/MugshotWindow.py:589 msgid "Mugshot" msgstr "" @@ -130,70 +130,82 @@ msgstr "" msgid "Retry" msgstr "" -#. Password was incorrect, complain. -#: ../mugshot/MugshotWindow.py:316 -msgid "Authentication Failed" +#: ../mugshot/MugshotWindow.py:321 +msgid "Authentication cancelled." msgstr "" -#: ../mugshot/MugshotWindow.py:317 +#: ../mugshot/MugshotWindow.py:324 +msgid "Authentication failed." +msgstr "" + +#: ../mugshot/MugshotWindow.py:327 +msgid "An error occurred when saving changes." +msgstr "" + +#: ../mugshot/MugshotWindow.py:329 msgid "User details were not updated." msgstr "" -#: ../mugshot/MugshotWindow.py:488 +#: ../mugshot/MugshotWindow.py:499 msgid "Update Pidgin buddy icon?" msgstr "" -#: ../mugshot/MugshotWindow.py:489 +#: ../mugshot/MugshotWindow.py:500 msgid "Would you also like to update your Pidgin buddy icon?" msgstr "" -#: ../mugshot/MugshotWindow.py:580 +#: ../mugshot/MugshotWindow.py:590 msgid "Enter your password to change user details." msgstr "" -#: ../mugshot/MugshotWindow.py:582 +#: ../mugshot/MugshotWindow.py:592 msgid "" "This is a security measure to prevent unwanted updates\n" "to your personal information." msgstr "" -#: ../mugshot/MugshotWindow.py:787 +#: ../mugshot/MugshotWindow.py:797 msgid "Update LibreOffice user details?" msgstr "" -#: ../mugshot/MugshotWindow.py:788 +#: ../mugshot/MugshotWindow.py:798 msgid "Would you also like to update your user details in LibreOffice?" msgstr "" -#: ../mugshot_lib/SudoDialog.py:80 +#: ../mugshot_lib/SudoDialog.py:120 +msgid "Password Required" +msgstr "" + +#: ../mugshot_lib/SudoDialog.py:157 +msgid "Incorrect password... try again." +msgstr "" + +#: ../mugshot_lib/SudoDialog.py:167 +msgid "Password:" +msgstr "" + +#. Buttons +#: ../mugshot_lib/SudoDialog.py:178 msgid "Cancel" msgstr "" -#: ../mugshot_lib/SudoDialog.py:81 +#: ../mugshot_lib/SudoDialog.py:181 msgid "OK" msgstr "" -#: ../mugshot_lib/SudoDialog.py:94 +#: ../mugshot_lib/SudoDialog.py:202 msgid "" "Enter your password to\n" "perform administrative tasks." msgstr "" -#: ../mugshot_lib/SudoDialog.py:96 +#: ../mugshot_lib/SudoDialog.py:204 #, python-format msgid "" "The application '%s' lets you\n" "modify essential parts of your system." msgstr "" -#: ../mugshot_lib/SudoDialog.py:117 -msgid "Incorrect password... try again." -msgstr "" - -#: ../mugshot_lib/SudoDialog.py:122 -msgid "Password:" -msgstr "" - #: ../data/appdata/mugshot.appdata.xml.in.h:1 msgid "Lightweight user configuration" msgstr ""