602 lines
22 KiB
Python
602 lines
22 KiB
Python
#!/usr/bin/python3
|
|
|
|
## system-config-printer
|
|
|
|
## Copyright (C) 2008, 2009, 2010, 2011, 2012, 2013, 2014 Red Hat, Inc.
|
|
## Authors:
|
|
## Tim Waugh <twaugh@redhat.com>
|
|
|
|
## 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 2 of the License, or
|
|
## (at your option) any later version.
|
|
|
|
## This program is distributed in the hope that it will be useful,
|
|
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
## GNU General Public License for more details.
|
|
|
|
## You should have received a copy of the GNU General Public License
|
|
## along with this program; if not, write to the Free Software
|
|
## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
import config
|
|
import gettext
|
|
gettext.install(domain=config.PACKAGE, localedir=config.localedir)
|
|
import cups
|
|
import dbus
|
|
from gi.repository import GObject
|
|
from gi.repository import Gtk
|
|
import os
|
|
import socket
|
|
import tempfile
|
|
import time
|
|
|
|
import authconn
|
|
from debug import *
|
|
from errordialogs import *
|
|
import firewallsettings
|
|
from gui import GtkGUI
|
|
|
|
try:
|
|
try_CUPS_SERVER_REMOTE_ANY = cups.CUPS_SERVER_REMOTE_ANY
|
|
except AttributeError:
|
|
# cups module was compiled with CUPS < 1.3
|
|
try_CUPS_SERVER_REMOTE_ANY = "_remote_any"
|
|
|
|
# Set up "Problems?" link button
|
|
class _UnobtrusiveButton(Gtk.Button):
|
|
def __init__ (self, **args):
|
|
Gtk.Button.__init__ (self, **args)
|
|
self.set_relief (Gtk.ReliefStyle.NONE)
|
|
label = self.get_child ()
|
|
text = label.get_text ()
|
|
label.set_use_markup (True)
|
|
label.set_markup ('<span size="small" ' +
|
|
'underline="single" ' +
|
|
'color="#0000ee">%s</span>' % text)
|
|
|
|
class ServerSettings(GtkGUI):
|
|
|
|
__gsignals__ = {
|
|
'settings-applied': (GObject.SignalFlags.RUN_LAST, None, ()),
|
|
'dialog-canceled': (GObject.SignalFlags.RUN_LAST, None, ()),
|
|
'problems-clicked': (GObject.SignalFlags.RUN_LAST, None, ()),
|
|
}
|
|
|
|
RESOURCE="/admin/conf/cupsd.conf"
|
|
|
|
def __init__ (self, host=None, encryption=None, parent=None):
|
|
GObject.GObject.__init__ (self)
|
|
self.cupsconn = authconn.Connection (host=host, encryption=encryption)
|
|
self._host = host
|
|
self._parent = parent
|
|
self.getWidgets({"ServerSettingsDialog":
|
|
["ServerSettingsDialog",
|
|
"chkServerBrowse",
|
|
"chkServerShare",
|
|
"chkServerShareAny",
|
|
"chkServerRemoteAdmin",
|
|
"chkServerAllowCancelAll",
|
|
"chkServerLogDebug",
|
|
"hboxServerBrowse",
|
|
"rbPreserveJobFiles",
|
|
"rbPreserveJobHistory",
|
|
"rbPreserveJobNone",
|
|
"tvBrowseServers",
|
|
"frameBrowseServers",
|
|
"btAdvServerAdd",
|
|
"btAdvServerRemove"]},
|
|
|
|
domain=config.PACKAGE)
|
|
|
|
problems = _UnobtrusiveButton (label=_("Problems?"))
|
|
self.hboxServerBrowse.pack_end (problems, False, False, 0)
|
|
problems.connect ('clicked', self.problems_clicked)
|
|
problems.show ()
|
|
|
|
self.ServerSettingsDialog.connect ('response', self.on_response)
|
|
|
|
# Signal handler IDs.
|
|
self.handler_ids = {}
|
|
|
|
self.dialog = self.ServerSettingsDialog
|
|
self.browse_treeview = self.tvBrowseServers
|
|
self.add = self.btAdvServerAdd
|
|
self.remove = self.btAdvServerRemove
|
|
|
|
selection = self.browse_treeview.get_selection ()
|
|
selection.set_mode (Gtk.SelectionMode.MULTIPLE)
|
|
self._connect (selection, 'changed', self.on_treeview_selection_changed)
|
|
|
|
for column in self.browse_treeview.get_columns():
|
|
self.browse_treeview.remove_column(column)
|
|
col = Gtk.TreeViewColumn ('', Gtk.CellRendererText (), text=0)
|
|
self.browse_treeview.append_column (col)
|
|
|
|
self._fillAdvanced ()
|
|
self._fillBasic ()
|
|
|
|
if parent:
|
|
self.dialog.set_transient_for (parent)
|
|
|
|
self.connect_signals ()
|
|
self.dialog.show ()
|
|
|
|
def get_dialog (self):
|
|
return self.dialog
|
|
|
|
def problems_clicked (self, button):
|
|
self.emit ('problems-clicked')
|
|
|
|
def _fillAdvanced(self):
|
|
# Fetch cupsd.conf
|
|
f = tempfile.TemporaryFile () # mode='w+b'
|
|
try:
|
|
self.cupsconn.getFile (self.RESOURCE, file=f)
|
|
except cups.HTTPError as e:
|
|
(s,) = e.args
|
|
show_HTTP_Error (s, self._parent)
|
|
raise
|
|
|
|
def parse_yesno (line):
|
|
arg1 = line.split (' ')[1].strip ()
|
|
if arg1 in ['true', 'on', 'enabled', 'yes']:
|
|
return True
|
|
if arg1 in ['false', 'off', 'disabled', 'no', '0']:
|
|
return False
|
|
try:
|
|
if int (arg1) != 0:
|
|
return True
|
|
except:
|
|
pass
|
|
raise RuntimeError
|
|
|
|
preserve_job_history = True
|
|
preserve_job_files = True
|
|
browsing = True
|
|
self.browse_poll = []
|
|
f.seek (0)
|
|
for line in f:
|
|
line = line.decode ('UTF-8')
|
|
l = line.lower ().strip ()
|
|
if l.startswith ("preservejobhistory "):
|
|
try:
|
|
preserve_job_history = parse_yesno (l)
|
|
except:
|
|
pass
|
|
elif l.startswith ("preservejobfiles "):
|
|
try:
|
|
preserve_job_files = parse_yesno (l)
|
|
except:
|
|
pass
|
|
elif l.startswith ("browsing "):
|
|
try:
|
|
browsing = parse_yesno (l)
|
|
except:
|
|
pass
|
|
elif l.startswith ("browsepoll "):
|
|
self.browse_poll.append (line[len ("browsepoll "):].strip ())
|
|
|
|
self.frameBrowseServers.set_sensitive (browsing)
|
|
|
|
if preserve_job_files:
|
|
self.rbPreserveJobFiles.set_active (True)
|
|
elif preserve_job_history:
|
|
self.rbPreserveJobHistory.set_active (True)
|
|
else:
|
|
self.rbPreserveJobNone.set_active (True)
|
|
|
|
self.preserve_job_history = preserve_job_history
|
|
self.preserve_job_files = preserve_job_files
|
|
|
|
model = Gtk.ListStore (str)
|
|
self.browse_treeview.set_model (model)
|
|
for server in self.browse_poll:
|
|
model.append (row=[server])
|
|
|
|
def _fillBasic(self):
|
|
self.changed = set()
|
|
self.cupsconn._begin_operation (_("fetching server settings"))
|
|
try:
|
|
self.server_settings = self.cupsconn.adminGetServerSettings()
|
|
except cups.IPPError as e:
|
|
(e, m) = e.args
|
|
show_IPP_Error(e, m, self._parent)
|
|
self.cupsconn._end_operation ()
|
|
raise
|
|
|
|
self.cupsconn._end_operation ()
|
|
|
|
for widget, setting in [
|
|
(self.chkServerBrowse, cups.CUPS_SERVER_REMOTE_PRINTERS),
|
|
(self.chkServerShare, cups.CUPS_SERVER_SHARE_PRINTERS),
|
|
(self.chkServerShareAny, try_CUPS_SERVER_REMOTE_ANY),
|
|
(self.chkServerRemoteAdmin, cups.CUPS_SERVER_REMOTE_ADMIN),
|
|
(self.chkServerAllowCancelAll, cups.CUPS_SERVER_USER_CANCEL_ANY),
|
|
(self.chkServerLogDebug, cups.CUPS_SERVER_DEBUG_LOGGING),]:
|
|
widget.setting = setting
|
|
if setting in self.server_settings:
|
|
widget.set_active(int(self.server_settings[setting]))
|
|
widget.set_sensitive(True)
|
|
widget.show()
|
|
else:
|
|
widget.set_active(False)
|
|
widget.set_sensitive(False)
|
|
widget.hide()
|
|
|
|
if cups.CUPS_SERVER_REMOTE_PRINTERS in self.server_settings:
|
|
self.frameBrowseServers.show()
|
|
else:
|
|
self.frameBrowseServers.hide()
|
|
|
|
try:
|
|
flag = cups.CUPS_SERVER_SHARE_PRINTERS
|
|
publishing = int (self.server_settings[flag])
|
|
self.server_is_publishing = publishing
|
|
except AttributeError:
|
|
pass
|
|
|
|
# Set sensitivity of 'Allow printing from the Internet'.
|
|
self.on_server_changed (self.chkServerShare) # (any will do here)
|
|
|
|
def on_server_changed(self, widget):
|
|
debugprint ("on_server_changed: %s" % widget)
|
|
setting = widget.setting
|
|
if setting in self.server_settings:
|
|
if str(int(widget.get_active())) == self.server_settings[setting]:
|
|
self.changed.discard(widget)
|
|
else:
|
|
self.changed.add(widget)
|
|
|
|
sharing = self.chkServerShare.get_active ()
|
|
self.chkServerShareAny.set_sensitive (
|
|
sharing and try_CUPS_SERVER_REMOTE_ANY in self.server_settings)
|
|
|
|
def _connect (self, widget, signal, handler, reason=None):
|
|
id = widget.connect (signal, handler)
|
|
if reason not in self.handler_ids:
|
|
self.handler_ids[reason] = []
|
|
self.handler_ids[reason].append ((widget, id))
|
|
|
|
def _disconnect (self, reason=None):
|
|
if reason in self.handler_ids:
|
|
for (widget, id) in self.handler_ids[reason]:
|
|
widget.disconnect (id)
|
|
del self.handler_ids[reason]
|
|
|
|
def on_treeview_selection_changed (self, selection):
|
|
self.remove.set_sensitive (selection.count_selected_rows () != 0)
|
|
|
|
def on_add_clicked (self, button):
|
|
model = self.browse_treeview.get_model ()
|
|
iter = model.insert (0, row=[_("Enter hostname")])
|
|
button.set_sensitive (False)
|
|
col = self.browse_treeview.get_columns ()[0]
|
|
cell = col.get_cells ()[0]
|
|
cell.set_property ('editable', True)
|
|
self.browse_treeview.set_cursor (Gtk.TreePath(), col, True)
|
|
self._connect (cell, 'edited', self.on_browse_poll_edited, 'edit')
|
|
self._connect (cell, 'editing-canceled',
|
|
self.on_browse_poll_edit_cancel, 'edit')
|
|
|
|
def on_browse_poll_edited (self, cell, path, newvalue):
|
|
model = self.browse_treeview.get_model ()
|
|
iter = model.get_iter (path)
|
|
model.set_value (iter, 0, newvalue)
|
|
cell.stop_editing (False)
|
|
cell.set_property ('editable', False)
|
|
self.add.set_sensitive (True)
|
|
self._disconnect ('edit')
|
|
|
|
valid = True
|
|
# Check that it's a valid IP address or hostname.
|
|
# First, is it an IP address?
|
|
try:
|
|
socket.getaddrinfo (newvalue, '0', socket.AF_UNSPEC, 0, 0,
|
|
socket.AI_NUMERICHOST)
|
|
except socket.gaierror:
|
|
# No. Perhaps it's a hostname.
|
|
labels = newvalue.split (".")
|
|
seen_alpha = False
|
|
for label in labels:
|
|
if (label[0] == '-' or
|
|
label.endswith ('-')):
|
|
valid = False
|
|
break
|
|
for char in label:
|
|
if not seen_alpha:
|
|
if char.isalpha ():
|
|
seen_alpha = True
|
|
|
|
if not (char.isalpha () or
|
|
char.isdigit () or
|
|
char == '-'):
|
|
valid = False
|
|
break
|
|
|
|
if not valid:
|
|
break
|
|
|
|
if valid and not seen_alpha:
|
|
valid = False
|
|
|
|
if valid:
|
|
count = 0
|
|
i = model.get_iter_first ()
|
|
while i:
|
|
if model.get_value (i, 0) == newvalue:
|
|
count += 1
|
|
if count == 2:
|
|
valid = False
|
|
selection = self.browse_treeview.get_selection ()
|
|
selection.select_iter (i)
|
|
break
|
|
i = model.iter_next (i)
|
|
else:
|
|
model.remove (iter)
|
|
|
|
def on_browse_poll_edit_cancel (self, cell):
|
|
cell.stop_editing (True)
|
|
cell.set_property ('editable', False)
|
|
model = self.browse_treeview.get_model ()
|
|
iter = model.get_iter (Gtk.TreePath())
|
|
model.remove (iter)
|
|
self.add.set_sensitive (True)
|
|
self.remove.set_sensitive (False)
|
|
self._disconnect ('edit')
|
|
|
|
def on_remove_clicked (self, button):
|
|
model = self.browse_treeview.get_model ()
|
|
selection = self.browse_treeview.get_selection ()
|
|
rows = selection.get_selected_rows ()
|
|
refs = [Gtk.TreeRowReference.new (model, path) for path in rows[1]]
|
|
for ref in refs:
|
|
path = ref.get_path ()
|
|
iter = model.get_iter (path)
|
|
model.remove (iter)
|
|
|
|
def on_response (self, dialog, response):
|
|
if (response == Gtk.ResponseType.CANCEL or
|
|
response != Gtk.ResponseType.OK):
|
|
self._disconnect ()
|
|
self.dialog.hide ()
|
|
self.emit ('dialog-canceled')
|
|
del self
|
|
return
|
|
|
|
self.saveBasic ()
|
|
self.saveAdvanced ()
|
|
|
|
def _reconnect (self):
|
|
# Now reconnect, in case the server needed to reload.
|
|
try:
|
|
attempt = 1
|
|
while attempt <= 5:
|
|
try:
|
|
self.cupsconn._connect ()
|
|
break
|
|
except RuntimeError:
|
|
# Connection failed.
|
|
time.sleep (1)
|
|
attempt += 1
|
|
except AttributeError:
|
|
# _connect method is part of the authconn.Connection
|
|
# interface, so don't fail if that method doesn't exist.
|
|
pass
|
|
|
|
def saveAdvanced (self):
|
|
# See if there are changes.
|
|
preserve_job_files = self.rbPreserveJobFiles.get_active ()
|
|
preserve_job_history = (preserve_job_files or
|
|
self.rbPreserveJobHistory.get_active ())
|
|
model = self.browse_treeview.get_model ()
|
|
browse_poll = []
|
|
iter = model.get_iter_first ()
|
|
while iter:
|
|
browse_poll.append (model.get_value (iter, 0))
|
|
iter = model.iter_next (iter)
|
|
|
|
if (set (browse_poll) == set (self.browse_poll) and
|
|
preserve_job_files == self.preserve_job_files and
|
|
preserve_job_history == self.preserve_job_history):
|
|
self._disconnect ()
|
|
self.dialog.hide ()
|
|
self.emit ('settings-applied')
|
|
del self
|
|
return
|
|
|
|
# Fetch cupsd.conf afresh
|
|
f = tempfile.TemporaryFile () # mode='w+b'
|
|
try:
|
|
self.cupsconn.getFile (self.RESOURCE, file=f)
|
|
except cups.HTTPError as e:
|
|
(s,) = e.args
|
|
show_HTTP_Error (s, self.dialog)
|
|
return
|
|
|
|
job_history_line = job_files_line = browsepoll_lines = ""
|
|
|
|
# Default is to preserve job history
|
|
if not preserve_job_history:
|
|
job_history_line = "PreserveJobHistory No\n"
|
|
|
|
# Default is to preserve job files.
|
|
if not preserve_job_files:
|
|
job_files_line = "PreserveJobFiles No\n"
|
|
|
|
for server in browse_poll:
|
|
browsepoll_lines += "BrowsePoll %s\n" % server
|
|
|
|
f.seek (0)
|
|
conf = tempfile.TemporaryFile () # mode='w+b'
|
|
wrote_preserve_history = wrote_preserve_files = False
|
|
wrote_browsepoll = False
|
|
has_browsepoll = False
|
|
for line in f:
|
|
line = line.decode('UTF-8')
|
|
l = line.lower ().strip ()
|
|
if l.startswith ("browsepoll "):
|
|
has_browsepoll = True
|
|
break
|
|
|
|
# Return to the start of file
|
|
f.seek(0)
|
|
|
|
for line in f:
|
|
line = line.decode('UTF-8')
|
|
l = line.lower ().strip ()
|
|
if l.startswith ("preservejobhistory "):
|
|
if wrote_preserve_history:
|
|
# Don't write out another line with this keyword.
|
|
continue
|
|
# Alter this line before writing it out.
|
|
line = job_history_line
|
|
wrote_preserve_history = True
|
|
elif l.startswith ("preservejobfiles "):
|
|
if wrote_preserve_files:
|
|
# Don't write out another line with this keyword.
|
|
continue
|
|
# Alter this line before writing it out.
|
|
line = job_files_line
|
|
wrote_preserve_files = True
|
|
elif (has_browsepoll and
|
|
l.startswith ("browsepoll ")):
|
|
if wrote_browsepoll:
|
|
# Ignore extra BrowsePoll lines.
|
|
continue
|
|
# Write new BrowsePoll section.
|
|
conf.write (browsepoll_lines.encode('UTF-8'))
|
|
wrote_browsepoll = True
|
|
# Don't write out the original BrowsePoll line.
|
|
continue
|
|
elif (not has_browsepoll and
|
|
l.startswith ("browsing ")):
|
|
if not wrote_browsepoll:
|
|
# Write original Browsing line.
|
|
conf.write (line.encode('UTF-8'))
|
|
# Write new BrowsePoll section.
|
|
conf.write (browsepoll_lines.encode('UTF-8'))
|
|
wrote_browsepoll = True
|
|
continue
|
|
|
|
conf.write (line.encode('UTF-8'))
|
|
|
|
if not wrote_preserve_history:
|
|
conf.write (job_history_line.encode('UTF-8'))
|
|
if not wrote_preserve_files:
|
|
conf.write (job_files_line.encode('UTF-8'))
|
|
if not wrote_browsepoll:
|
|
conf.write (browsepoll_lines.encode('UTF-8'))
|
|
|
|
conf.flush ()
|
|
fd = conf.fileno ()
|
|
os.lseek (fd, 0, os.SEEK_SET)
|
|
try:
|
|
self.cupsconn.putFile ("/admin/conf/cupsd.conf", fd=fd)
|
|
except cups.IPPError as e:
|
|
(e, m) = e.args
|
|
show_IPP_Error (e, m, self.dialog)
|
|
return
|
|
except cups.HTTPError as e:
|
|
(s,) = e.args
|
|
show_HTTP_Error (s, self.dialog)
|
|
return
|
|
|
|
# Give the server a chance to process our request.
|
|
time.sleep (1)
|
|
|
|
self._reconnect ()
|
|
|
|
self._disconnect ()
|
|
self.emit ('settings-applied')
|
|
self.dialog.hide ()
|
|
del self
|
|
|
|
def saveBasic (self):
|
|
setting_dict = dict()
|
|
for widget, setting in [
|
|
(self.chkServerBrowse, cups.CUPS_SERVER_REMOTE_PRINTERS),
|
|
(self.chkServerShare, cups.CUPS_SERVER_SHARE_PRINTERS),
|
|
(self.chkServerShareAny, try_CUPS_SERVER_REMOTE_ANY),
|
|
(self.chkServerRemoteAdmin, cups.CUPS_SERVER_REMOTE_ADMIN),
|
|
(self.chkServerAllowCancelAll, cups.CUPS_SERVER_USER_CANCEL_ANY),
|
|
(self.chkServerLogDebug, cups.CUPS_SERVER_DEBUG_LOGGING),]:
|
|
if setting not in self.server_settings: continue
|
|
setting_dict[setting] = str(int(widget.get_active()))
|
|
self.cupsconn._begin_operation (_("modifying server settings"))
|
|
try:
|
|
self.cupsconn.adminSetServerSettings(setting_dict)
|
|
except cups.IPPError as e:
|
|
(e, m) = e.args
|
|
show_IPP_Error(e, m, self.dialog)
|
|
self.cupsconn._end_operation ()
|
|
return True
|
|
except RuntimeError as s:
|
|
show_IPP_Error(None, s, self.dialog)
|
|
self.cupsconn._end_operation ()
|
|
return True
|
|
self.cupsconn._end_operation ()
|
|
self.changed = set()
|
|
|
|
old_setting = self.server_settings.get (cups.CUPS_SERVER_SHARE_PRINTERS,
|
|
'0')
|
|
new_setting = setting_dict.get (cups.CUPS_SERVER_SHARE_PRINTERS, '0')
|
|
if (old_setting == '0' and new_setting != '0'):
|
|
# We have just enabled print queue sharing.
|
|
# Let's see if the firewall will allow IPP TCP packets in.
|
|
try:
|
|
if (self._host == 'localhost' or
|
|
self._host[0] == '/'):
|
|
f = firewallsettings.FirewallD ()
|
|
if not f.running:
|
|
f = firewallsettings.SystemConfigFirewall ()
|
|
|
|
allowed = f.check_ipp_server_allowed ()
|
|
else:
|
|
# This is a remote server. Nothing we can do
|
|
# about the firewall there.
|
|
allowed = True
|
|
|
|
if not allowed:
|
|
dialog = Gtk.MessageDialog (parent=self.ServerSettingsDialog,
|
|
modal=True, destroy_with_parent=True,
|
|
message_type=Gtk.MessageType.QUESTION,
|
|
buttons=Gtk.ButtonsType.NONE,
|
|
text=_("Adjust Firewall"))
|
|
dialog.format_secondary_text (_("Adjust the firewall now "
|
|
"to allow all incoming IPP "
|
|
"connections?"))
|
|
dialog.add_buttons (Gtk.STOCK_CANCEL, Gtk.ResponseType.NO,
|
|
_("Adjust Firewall"), Gtk.ResponseType.YES)
|
|
response = dialog.run ()
|
|
dialog.destroy ()
|
|
|
|
if response == Gtk.ResponseType.YES:
|
|
f.add_service (firewallsettings.IPP_SERVER_SERVICE)
|
|
f.write ()
|
|
except (dbus.DBusException, Exception):
|
|
nonfatalException ()
|
|
|
|
time.sleep(1) # give the server a chance to process our request
|
|
|
|
# Now reconnect, in case the server needed to reload.
|
|
self._reconnect ()
|
|
|
|
if __name__ == '__main__':
|
|
os.environ['SYSTEM_CONFIG_PRINTER_UI'] = 'ui'
|
|
loop = GObject.MainLoop ()
|
|
|
|
def quit (*args):
|
|
loop.quit ()
|
|
|
|
def problems (obj):
|
|
print("%s: problems" % obj)
|
|
|
|
set_debugging (True)
|
|
s = ServerSettings ()
|
|
s.connect ('dialog-canceled', quit)
|
|
s.connect ('settings-applied', quit)
|
|
s.connect ('problems-clicked', problems)
|
|
loop.run ()
|