diff -up system-config-printer-1.0.5/authconn.py.git system-config-printer-1.0.5/authconn.py --- system-config-printer-1.0.5/authconn.py.git 2008-08-11 14:33:09.000000000 +0100 +++ system-config-printer-1.0.5/authconn.py 2008-08-14 16:23:49.000000000 +0100 @@ -130,7 +130,13 @@ class Connection: def _authloop (self, fname, fn, *args, **kwds): self._passes = 0 + c = self._connection while self._perform_authentication () != 0: + if c != self._connection: + # We have reconnected. + fn = getattr (self._connection, fname) + c = self._connection + try: result = fn.__call__ (*args, **kwds) @@ -146,15 +152,17 @@ class Connection: else: raise except cups.HTTPError, (s,): - if not self._cancel and s == cups.HTTP_UNAUTHORIZED: - self._failed () + if not self._cancel and (s == cups.HTTP_UNAUTHORIZED or + s == cups.HTTP_FORBIDDEN): + self._failed (s == cups.HTTP_FORBIDDEN) else: raise return result - def _failed (self): + def _failed (self, forbidden=False): self._has_failed = True + self._forbidden = forbidden def _password_callback (self, prompt): debugprint ("Got password callback") @@ -173,6 +181,7 @@ class Connection: # Haven't yet tried the operation. Set the password # callback and return > 0 so we try it for the first time. self._has_failed = False + self._forbidden = False self._auth_called = False self._cancel = False cups.setPasswordCB (self._password_callback) @@ -192,16 +201,17 @@ class Connection: # Tried the operation without a password and it failed. if (self._try_as_root and self._user != 'root' and - self._server[0] == '/'): + (self._server[0] == '/' or self._forbidden)): # This is a UNIX domain socket connection so we should - # not have needed a password, and so the operation must - # not be something that the current user is authorised to - # do. They need to try as root, and supply the password. - # However, to get the right prompt, we need to try as - # root but with no password first. + # not have needed a password (or it is not a UDS but + # we got an HTTP_FORBIDDEN response), and so the + # operation must not be something that the current + # user is authorised to do. They need to try as root, + # and supply the password. However, to get the right + # prompt, we need to try as root but with no password + # first. debugprint ("Authentication: Try as root") self._use_user = 'root' - cups.setUser (self._use_user) self._auth_called = False self._connect () return 1 diff -U0 system-config-printer-1.0.5/ChangeLog.git system-config-printer-1.0.5/ChangeLog --- system-config-printer-1.0.5/ChangeLog.git 2008-08-11 14:51:07.000000000 +0100 +++ system-config-printer-1.0.5/ChangeLog 2008-08-14 16:23:49.000000000 +0100 @@ -0,0 +1,67 @@ +2008-08-14 Tim Waugh + + * cupshelpers/openprinting.py + (OpenPrinting.listDrivers.parse_result): OpenPrinting API change: + freesoftware -> nonfreesoftware (trac #74). + * system-config-printer.py + (NewPrinterGUI.on_tvNPDownloadableDrivers_cursor_changed): + Likewise. + +2008-08-14 Tim Waugh + + * jobviewer.py (JobViewer.on_new_printer_notification_closed): + Optional reason parameter to handle newer libnotify signal + interface. + (JobViewer.on_state_reason_notification_closed): Likewise. + (JobViewer.on_auth_notification_closed): Likewise. + +2008-08-14 Tim Waugh + + * jobviewer.py (JobViewer.update_job): Display a notification when + a job requires authentication (trac #91). + (JobViewer.on_auth_notification_authenticate): New method. + Display an authentication dialog when the notification's + authenticate action is triggered. + (JobViewer.on_auth_notification_closed): New method. Handle an + auth notification being closed. + +2008-08-14 Tim Waugh + + * jobviewer.py (JobViewer.update_job): Moved code for displaying + authentication dialog... + (JobViewer.display_auth_info_dialog): ...here. New method. + +2008-08-14 Rui Tiago Cação Matos + + * system-config-printer.py (GUI.__init__): + * system-config-printer.py (NewPrinterGUI.__init__): Since some + dialogs are reused we can't let the delete-event's default handler + destroy them. + * system-config-printer.py (on_delete_just_hide): This method is + connected to those dialog's delete-event (trac #88). + +2008-08-14 Tim Waugh + + * jobviewer.py (JobViewer.update_job): When a job is held for + authentication, say so in the status column. + +2008-08-14 Tim Waugh + + * jobviewer.py (JobViewer.update_job): Show authentication dialog + in the middle of the screen (trac #90). + +2008-08-13 Tim Waugh + + * authconn.py (Connection._authloop): Re-bind to the named method + on reconnection. Handle HTTP_FORBIDDEN (trac #89). + (Connection._failed): New optional parameter forbidden. Remember + whether we saw HTTP_FORBIDDEN. + (Connection._perform_authentication): Initialise self._forbidden. + Use it to decide whether to try as root. Don't call setUser() + here; _connect() will do that. + +2008-08-12 Tim Waugh + + * jobviewer.py (JobViewer.update_job): Pre-fill the username field + when asking for authentication for the job (trac #87). + diff -up system-config-printer-1.0.5/cupshelpers/openprinting.py.git system-config-printer-1.0.5/cupshelpers/openprinting.py --- system-config-printer-1.0.5/cupshelpers/openprinting.py.git 2008-08-11 10:05:02.000000000 +0100 +++ system-config-printer-1.0.5/cupshelpers/openprinting.py 2008-08-14 16:23:49.000000000 +0100 @@ -217,7 +217,7 @@ class OpenPrinting: # 'supplier': supplier, # 'license': short license string e.g. GPLv2, # 'licensetext': license text (HTML), - # 'freesoftware': Boolean, + # 'nonfreesoftware': Boolean, # 'patents': Boolean, # 'shortdescription': short description, # 'recommended': Boolean, @@ -260,10 +260,15 @@ class OpenPrinting: if element != None: dict['licensetext'] = element.text - for boolean in ['freesoftware', 'recommended', + for boolean in ['nonfreesoftware', 'recommended', 'patents']: dict[boolean] = driver.find (boolean) != None + # Make a 'freesoftware' tag for compatibility with + # how the OpenPrinting API used to work (see trac + # #74). + dict['freesoftware'] = not dict['nonfreesoftware'] + if not dict.has_key ('name') or not dict.has_key ('url'): continue diff -up system-config-printer-1.0.5/jobviewer.py.git system-config-printer-1.0.5/jobviewer.py --- system-config-printer-1.0.5/jobviewer.py.git 2008-08-11 14:51:07.000000000 +0100 +++ system-config-printer-1.0.5/jobviewer.py 2008-08-14 16:23:49.000000000 +0100 @@ -31,6 +31,7 @@ import gtk.glade import monitor import os import pango +import pwd import sys import time @@ -160,6 +161,7 @@ class JobViewer (monitor.Watcher): self.num_jobs_when_hidden = 0 self.connecting_to_device = {} # dict of printer->time first seen self.state_reason_notifications = {} + self.auth_notifications = {} self.job_creation_times_timer = None self.special_status_icon = False self.new_printer_notifications = {} @@ -320,7 +322,7 @@ class JobViewer (monitor.Watcher): notification.attach_to_status_icon (self.statusicon) notification.show () - def on_new_printer_notification_closed (self, notification): + def on_new_printer_notification_closed (self, notification, reason=None): printer = notification.get_data ('printer-name') del self.new_printer_notifications[printer] self.set_statusicon_visibility () @@ -471,17 +473,24 @@ class JobViewer (monitor.Watcher): store.set_value (iter, 4, size) state = None + job_requires_auth = False if data.has_key ('job-state'): try: jstate = data['job-state'] s = int (jstate) - state = { cups.IPP_JOB_PENDING: _("Pending"), - cups.IPP_JOB_HELD: _("Held"), - cups.IPP_JOB_PROCESSING: _("Processing"), - cups.IPP_JOB_STOPPED: _("Stopped"), - cups.IPP_JOB_CANCELED: _("Canceled"), - cups.IPP_JOB_ABORTED: _("Aborted"), - cups.IPP_JOB_COMPLETED: _("Completed") }[s] + job_requires_auth = (jstate == cups.IPP_JOB_HELD and + data.get ('job-hold-until', 'none') == + 'auth-info-required') + if job_requires_auth: + state = _("Held for authentication") + else: + state = { cups.IPP_JOB_PENDING: _("Pending"), + cups.IPP_JOB_HELD: _("Held"), + cups.IPP_JOB_PROCESSING: _("Processing"), + cups.IPP_JOB_STOPPED: _("Stopped"), + cups.IPP_JOB_CANCELED: _("Canceled"), + cups.IPP_JOB_ABORTED: _("Aborted"), + cups.IPP_JOB_COMPLETED: _("Completed") }[s] except ValueError: pass except IndexError: @@ -492,64 +501,115 @@ class JobViewer (monitor.Watcher): store.set_value (iter, 6, state) # Check whether authentication is required. - if (self.trayicon and - data['job-state'] == cups.IPP_JOB_HELD and - data.get ('job-hold-until', 'none') == 'auth-info-required' and - not self.auth_info_dialog): + if self.trayicon: + if (job_requires_auth and + not self.auth_notifications.has_key (job) and + not self.auth_info_dialog): + try: + cups.require ("1.9.37") + except: + debugprint ("Authentication required but " + "authenticateJob() not available") + return + + title = _("Authentication Required") + text = _("Job requires authentication to proceed.") + notification = pynotify.Notification (title, text, 'printer') + notification.set_data ('job-id', job) + notification.set_urgency (pynotify.URGENCY_NORMAL) + notification.set_timeout (pynotify.EXPIRES_NEVER) + notification.connect ('closed', + self.on_auth_notification_closed) + self.set_statusicon_visibility () + notification.attach_to_status_icon (self.statusicon) + notification.add_action ("authenticate", _("Authenticate"), + self.on_auth_notification_authenticate) + notification.show () + self.auth_notifications[job] = notification + elif (not job_requires_auth and + self.auth_notifications.has_key (job)): + self.auth_notifications[job].close () + + def on_auth_notification_closed (self, notification, reason=None): + job = notification.get_data ('job-id') + debugprint ("auth notification closed for job %s" % job) + del self.auth_notifications[job] + + def on_auth_notification_authenticate (self, notification, action): + job = notification.get_data ('job-id') + debugprint ("auth notification authenticate for job %s" % job) + self.display_auth_info_dialog (job) + + def display_auth_info_dialog (self, job): + data = self.jobs[job] + # Find out which auth-info is required. + try: + c = authconn.Connection (self.MainWindow) try: - cups.require ("1.9.37") - except: - debugprint ("Authentication required but " - "authenticateJob() not available") - return + uri = data['job-printer-uri'] + attributes = c.getPrinterAttributes (uri = uri) + except TypeError: # uri keyword introduced in pycups-1.9.32 + debugprint ("Fetching printer attributes by name") + attributes = c.getPrinterAttributes (printer) + except cups.IPPError, (e, m): + self.show_IPP_Error (e, m) + return + except RuntimeError: + debugprint ("Failed to connect when fetching printer attrs") + return - # Find out which auth-info is required. + try: + auth_info_required = attributes['auth-info-required'] + except KeyError: + debugprint ("No auth-info-required attribute; guessing instead") + auth_info_required = ['username', 'password'] + + if not isinstance (auth_info_required, list): + auth_info_required = [auth_info_required] + + if auth_info_required == ['negotiate']: + # Try Kerberos authentication. try: - c = authconn.Connection (self.MainWindow) - try: - uri = data['job-printer-uri'] - attributes = c.getPrinterAttributes (uri = uri) - except TypeError: # uri keyword introduced in pycups-1.9.32 - debugprint ("Fetching printer attributes by name") - attributes = c.getPrinterAttributes (printer) + debugprint ("Trying Kerberos auth for job %d" % jobid) + c.authenticateJob (jobid) + except TypeError: + # Requires pycups-1.9.39 for optional auth parameter. + # Treat this as a normal job error. + debugprint ("... need newer pycups for that") + return except cups.IPPError, (e, m): self.show_IPP_Error (e, m) return - except RuntimeError: - debugprint ("Failed to connect when fetching printer attrs") - return - try: - auth_info_required = attributes['auth-info-required'] - except KeyError: - debugprint ("No auth-info-required attribute; guessing instead") - auth_info_required = ['username', 'password'] - - if not isinstance (auth_info_required, list): - auth_info_required = [auth_info_required] + dialog = authconn.AuthDialog (auth_info_required=auth_info_required) + dialog.set_position (gtk.WIN_POS_CENTER) - if auth_info_required == ['negotiate']: - # Try Kerberos authentication. - try: - debugprint ("Trying Kerberos auth for job %d" % jobid) - c.authenticateJob (jobid) - except TypeError: - # Requires pycups-1.9.39 for optional auth parameter. - # Treat this as a normal job error. - debugprint ("... need newer pycups for that") - return - except cups.IPPError, (e, m): - self.show_IPP_Error (e, m) - return + # Pre-fill 'username' field. + if 'username' in auth_info_required: + try: + auth_info = map (lambda x: '', auth_info_required) + username = pwd.getpwuid (os.getuid ())[0] + ind = auth_info_required.index ('username') + auth_info[ind] = username + dialog.set_auth_info (auth_info) + + index = 0 + for field in auth_info_required: + if auth_info[index] == '': + # Focus on the first empty field. + dialog.field_grab_focus (field) + break + index += 1 + except: + nonfatalException () - dialog = authconn.AuthDialog (auth_info_required=auth_info_required) - dialog.set_prompt (_("Authentication required for " - "printing document `%s' (job %d)") % - (data.get('job-name', _("Unknown")), job)) - self.auth_info_dialog = dialog - dialog.connect ('response', self.auth_info_dialog_response) - dialog.set_data ('job-id', job) - dialog.show_all () + dialog.set_prompt (_("Authentication required for " + "printing document `%s' (job %d)") % + (data.get('job-name', _("Unknown")), job)) + self.auth_info_dialog = dialog + dialog.connect ('response', self.auth_info_dialog_response) + dialog.set_data ('job-id', job) + dialog.show_all () def auth_info_dialog_response (self, dialog, response): dialog.hide () @@ -862,7 +922,7 @@ class JobViewer (monitor.Watcher): notification.attach_to_status_icon (self.statusicon) notification.show () - def on_state_reason_notification_closed (self, notification): + def on_state_reason_notification_closed (self, notification, reason=None): debugprint ("Notification %s closed" % repr (notification)) reason = notification.get_data ('printer-state-reason') tuple = reason.get_tuple () diff -up system-config-printer-1.0.5/system-config-printer.py.git system-config-printer-1.0.5/system-config-printer.py --- system-config-printer-1.0.5/system-config-printer.py.git 2008-08-11 14:51:07.000000000 +0100 +++ system-config-printer-1.0.5/system-config-printer.py 2008-08-14 16:23:49.000000000 +0100 @@ -152,6 +152,10 @@ class GtkGUI: result.sort() return result +def on_delete_just_hide (widget, event): + widget.hide () + return True # stop other handlers + class GUI(GtkGUI, monitor.Watcher): printer_states = { cups.IPP_PRINTER_IDLE: _("Idle"), @@ -271,6 +275,14 @@ class GUI(GtkGUI, monitor.Watcher): "AboutDialog", "WaitWindow", "lblWait", ) + + # Since some dialogs are reused we can't let the delete-event's + # default handler destroy them + for dialog in [self.PrinterPropertiesDialog, + self.ServerSettingsDialog, + self.ConnectingDialog]: + dialog.connect ("delete-event", on_delete_just_hide) + self.tooltips = gtk.Tooltips() self.tooltips.enable() gtk.window_set_default_icon_name ('printer') @@ -2597,6 +2609,13 @@ class NewPrinterGUI(GtkGUI): "rbtnNPDownloadLicenseNo", "NewPrinterName", "entCopyName", "btnCopyOk", "InstallDialog", "lblInstall") + + # Since some dialogs are reused we can't let the delete-event's + # default handler destroy them + for dialog in [self.IPPBrowseDialog, + self.SMBBrowseDialog]: + dialog.connect ("delete-event", on_delete_just_hide) + # share with mainapp self.WaitWindow = mainapp.WaitWindow self.lblWait = mainapp.lblWait @@ -4722,7 +4741,7 @@ class NewPrinterGUI(GtkGUI): self.lblNPDownloadableDriverLicense.set_text (license) description = driver.get('shortdescription', _("None")) self.lblNPDownloadableDriverDescription.set_text (description) - if driver['freesoftware'] and not driver['patents']: + if not driver['nonfreesoftware'] and not driver['patents']: self.rbtnNPDownloadLicenseYes.set_active (True) self.frmNPDownloadableDriverLicenseTerms.hide () else: