#!/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 = False
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 not to preserve job files.
if preserve_job_files:
job_files_line = "PreserveJobFiles Yes\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 ()