#!/usr/bin/python -O

# Preface
# -------
# This program attempts to be written in the literate style created an popularized by Donald E. Knuth. It fails in the
# attempt, but the attempt is made. It was written using vim, with python highlighting, at a screen width of 132
# characters, and uses tabs for indentation.


# General Modules
# ---------------
#
# It is difficult to write anything useful in python that does not use these modules, and there is not simple
# way to justify them, so we just import them. Strings must be munged, and files must be pathed.
import string
import re
import os
import sys


# Program Name, License, Version, and Other Details
# ----------------------------------------
#
# This program is copyright:
program_copyright = "Red Hat, Inc"
# and caries the following license:

	## printconf-backend
	## Copyright (C) 2000 Red Hat, Inc.
	## Copyright (C) 2000 Crutcher Dunnavant <crutcher@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., 675 Mass Ave, Cambridge, MA 02139, USA.

program_name = 'printconf-backend'
# this program is the backend core of the printconf system. It is called by lpd.init to rebuild filtration systems,
# spool directories, and printcap files.

program_version = '0.2.15'
# The version pattern follows the linux kernel style, with major.minor.revision, 'stable' branches on the even minor,
# and 'development' branches on the odd minor numbers.

program_authors = [	"Crutcher Dunnavant",		# The primary author (me)
			"The Lumberjacks of the World"] # A bit of humor about printing


# Xyzzy
# -----
#
# Hello Interseted Reader,
# If you've bothered to look at the code, I think I should point out some magic tricks which currently work, but are
# not guaranteed for the future. First, the file /etc/alchemist/namespace/printconf/local.adl 'is' your local printer
# configuration, and cloning it to another machine clones the configuration. The file, as well as all other *.adl
# files, is simply gziped XML. The recipie for it's creation is used by this program, and is called the switchboard
# file. If you get to understanding how to edit that file, truly miraculous things are possible in distributed
# configurations, but that functionality is not 'ready to wear' yet.
#
# Happy Hacking,
# 	- Crutcher


# Debug State
# -----------
#
# Some features and behaviours of this program are not meant for general use, but exist solely to aid in actually
# working upon the program. Python uses the specail variable '__debug__' to regulate some debuging behaviour, and we
# can hang the figurative hat of the rest of our debuging behaviours upon the same variable. If the python interpreter
# is set to generate optimized code, all blocks of the form:
#
#	if __debug__:
#		... block ...
#
# will be thrown away at /parse time/. This means that if the command line option -O is used on the shebang line
# above, they will never be executed, no matter __debug__'s value. It's value is set to 1 by default, and 0 by the
# optimize switch, and we set it here to be 'sure', but we can never turn debugging back on, if it was turned off by
# the shebang line.
__debug__ = 0

# Through out the program, it is useful to print some status information to the terminal if we are in debug mode.
# This function makes that a simple call.
def debug_print(str):
	if __debug__:
		print str

# This function is a wrapper to make it easier to print out the names of internal function calls.
def called(func):
	debug_print('Called : %s()' % func.__name__)


# Program Data Paths
# ------------------
#
# We need to know where some variable things are.
printconf_dir = "/usr/share/printconf"
foomatic_dir = printconf_dir + '/data'
lpd_spool_dir = "/var/spool/lpd"


# File Headers
# ------------
#
# The volatile file is a marker left in a spool directory saying, in effect, 'I was created by printconf'. If
# printconf finds this file in a directory it cannot account for, it will destroy the directory.
volatile_header = """THIS DIRECTORY IS VOLATILE!!!

This directory was created by printconf for a printconf spool.
If printconf-backend does a spool rebuild, and finds volatile
directories that do not currently have valid configurations,
it will DELETE THEM!

printconf-backend detects volatility by the presence of this file.
If you have custom spool directories made in some other manor, they
will be safe from printconf-backend's deletion as long as they do
NOT have a file in them named 'VOLATILE'.

"""

printcap_header = """# /etc/printcap
#
# DO NOT EDIT! MANUAL CHANGES WILL BE LOST!
# This file is autogenerated by printconf-backend during lpd init.
#
# Hand edited changes can be put in /etc/printcap.local, and will be included.

"""

printcap_local_preamble = """###############################################################################
## Everything below here is included verbatim from /etc/printcap.local       ##
###############################################################################
"""

printcap_local_header = """# /etc/printcap.local
#
# This file is included by printconf's generated printcap,
# and can be used to specify custom printcap entries.

"""

# printcap.local
# --------------
#
# /etc/printcap is generated; /etc/printcap.local is not, though it is imported by /etc/printcap. We make sure that
# /etc/printcap.local exists, but otherwise ignore it.
if not os.path.exists('/etc/printcap.local'):
	pl = open("/etc/printcap.local", "w")
	pl.write(printcap_local_header)
	pl.close()

# printconf alchemist switchboard
# -------------------------------
#
# The printconf system uses the alchemist data library to store and merge its configuration. To access the library, we
# load the python bindings, and pull in the switchboard file that describes the 'recipie' for the printconf context.
import Alchemist
switchboard = Alchemist.Switchboard(file_path = "/etc/alchemist/switchboard/printconf.switchboard.adl")


# To Rebuild, or Not
# ------------------
#
# Rebuilding the printing system can be expensive, and we want to avoid frivolus rebuilds. On the other hand, taking
# the time to confirm that the existing configuration is valid is also expensive, so we do not rescan the directories
# if the configuration looks current. To check, we look for some force flags, and then examine the switchboard.
rebuild = 0
if not rebuild:
	if os.environ.get('PRINTCONF_FORCE_REBUILD',None):
		debug_print("Rebuilding: Found 'PRINTCONF_FORCE_REBUILD' environment variable")
		rebuild = 1

if not rebuild:
	try:
		sys.argv.index('--force-rebuild')
		debug_print("Rebuilding: Found '--force-rebuild' command line option")
		rebuild = 1
	except:
		pass

if not rebuild:
	# Obviously rebuild if the printcap file is not there.
	if not os.path.exists('/etc/printcap'):
		debug_print("Rebuilding: '/etc/printcap' does not exist")
		rebuild = 1

if not rebuild:
	# Rebuild if the printcap.local file is newer than the printcap file
	if os.stat('/etc/printcap')[9] < os.stat('/etc/printcap.local')[9]:
		debug_print("Rebuilding: '/etc/printcap.local' is newer than '/etc/printcap'")
		rebuild = 1

if not rebuild:
	# rebuild if the namespace is dirty
	if switchboard.isNamespaceDirty('printconf'):
		debug_print("Rebuilding: printconf namespace is dirty")
		rebuild = 1

# Scan printcap
# -------------
#
# In the scenario that we are not forced to rebuild, we need to know if the existing printcap files have a printer
# defined. This is a tricky task, and we can't take the time to fully validate them. We just itterate through the
# databases, and take note if we ever find something that looks like a printer.
def scan_printcap_files(printcap_files):
	called(scan_printcap_files)

	while len(printcap_files):
		# get the next printcap file to scan
		file = printcap_files.pop()
	
		# address the issue of relative paths.
		if file[0] != '/':
			file = '/etc/' + file
	
		debug_print('scanning %s' % file)

		if not os.path.exists(file):
			# the printcap file in question does not exist, and attempting to start the server would fail.
			# However, if we fail here, we effectively swallow the error message that the print server will
			# throw (and will be more informative). So we just skip this file.
			continue
		
		for line in open(file).readlines():
			match = re.match(r'^\s*include\s*(?P<include>.*)\n?$', line)
			if match:
				# We've found an include file directive
				include = match.group('include')
				debug_print('found include directive: %s' % include)
				printcap_files.append(include)
				continue
			
			if re.match(r'^\s*\w', line):
				# We've found a valid printcap entry, return true.
				debug_print('found a printer line:\n%s' % line)
				return 1
	return None

# If we are not rebuilding, then the appropriate return value is dependent upon whatever scan_printcap_files finds.
if not rebuild:
	debug_print("Not rebuilding, scanning printcap files for entries")
	if scan_printcap_files(['/etc/printcap']):
		debug_print('Exit: Success')
		sys.exit(0)
	else:
		debug_print('Exit: Failure')
		sys.exit(1)


# Rebuilding ...
# --------------
#
# After the early exit case above, from this point on we know that we need to rebuild the print queues, and will no
# longer check the status of the rebuild variable.


# The alchemist context
# ---------------------
#
# To get the current configuration, we do a pull from the switchboard initialized earlier. If it fails, we scan the
# printcap files, and exit on the return value returned.
context = switchboard.writeNamespace('printconf', force = 1)
if not context:
	if scan_printcap_files(['/etc/printcap']):
		debug_print('Exit: Success')
		sys.exit(0)
	else:
		debug_print('Exit: Failure')
		sys.exit(1)

# Now that we've got the context, extract the print queue list from it, and reorder for the default printer, if one is
# defined and available.
print_queues = context.getDataByPath('/printconf/print_queues')
try:
	child = print_queues.getChildByName(context.getDataByPath('/printconf/default_queue').getValue())
	print_queues.moveChild(child, 0)
except:
	pass


# The Queue List
# --------------
#
# We extract the list of queues to process from the printconf context
queue_list = []
for i in range(print_queues.getNumChildren()):
	queue_list.append(print_queues.getChildByIndex(i))

# and strip out all conflicting aliases, to minimize the impact of alias merges from multiple sources.
# First, get a list of all the queue names
name_list = []
for queue in queue_list:
	try:
		queue.getChildByName('alias_list')
		queue.getChildByName('queue_type')
		queue.getChildByName('queue_data')
		queue.getChildByName('filter_type')
		queue.getChildByName('filter_data')
	except:
		# mallformed queue, zap it.
		queue_list.remove(queue)
		queue.unlink()
		continue

	name_list.append(queue.getName())

# Next, scan each queue's alias list, add new aliases to the name list and zap pre-defined aliases
for queue in queue_list:
	delete_set = []
	alias_list = queue.getChildByName('alias_list')
	for i in xrange(alias_list.getNumChildren()):
		alias = alias_list.getChildByIndex(i)
		str = alias.getValue()
		try:
			name_list.index(str)
			delete_set.append(alias)
		except ValueError:
			name_list.append(str)

	# Unlink every alias in the delete set
	for each in delete_set:
		each.unlink()


# The 'lp' user and group
# -----------------------
#
# Printer spools and files are owned by the 'lp' user and the 'lp' group. Since we will be creating these files, it is
# appropriate if we know the respective uid and gid of those groups. The pwd module is perfect for this lookup.
import pwd
(lp_uid, lp_gid) = pwd.getpwnam("lp")[2:4]


# Paranoid File Writes
# --------------------
#
# At a number of places in the program, it becomes necessary to write out a file that must be secure from the moment of
# creation. Since this is a bit annoying to do in python, I've got this nice little helper function for exactly this
# task.
def paranoid_file_write(file_name, str, mode, uid, gid):
	called(paranoid_file_write)

	if os.path.exists(file_name):
		os.unlink(file_name)
	fd = os.open(file_name, os.O_WRONLY | os.O_CREAT, mode)
	file = os.fdopen(fd, "w")
	file.write(str)
	file.close()
	os.chown(file_name, uid, gid)


# Magicfilter Configuration
# -------------------------
#
# Because most of our magicfilter cases are handled by mfomatic at this point, all magicfilter configuration gets
# passed on to a seccond stage perl script, which has the capability to read the foomatic files that mfomatic uses.
# This function builds the control structure that the second stage evals as it's input.
def magicfilter_cfg(spool_dir, filter_data):
	called(magicfilter_cfg)

	# This function builds a control structure to pass to a perl script
	# which can easily override the foomatic data file's options.
	#
	# Maybe if you don't think about it, it wont disturb you ;)
	p_cont = '$VAR1 = {'

	# Universal values
	mf_type = filter_data.getChildByName("mf_type").getValue()

	# Read the flags list
	p_cont = p_cont + "'flags'=>{"
	flags = filter_data.getChildByName('flags')
	for i in range(flags.getNumChildren()):
		flag = flags.getChildByIndex(i)
		p_cont = "%s '%s'=>'%s'," % (p_cont, flag.getName(), str(flag.getValue()) )
	p_cont = p_cont + "},"

	# We start with the assumption of 'Letter' as the page size. If we find something to override this, we change.
	page_size = 'Letter'

	# mfomatic values
	if mf_type == 'POSTSCRIPT':
		page_size = filter_data.getChildByName('page_size').getValue()

	elif mf_type == "MFOMATIC":
		printer_id = filter_data.getChildByName('printer_id').getValue()
		gs_driver = filter_data.getChildByName('gs_driver').getValue()
		p_cont = p_cont + "'printer_id'=>'%s','gs_driver'=>'%s'," % (printer_id, gs_driver) 

		p_cont = p_cont + "'foomatic_defaults' => {"
		foo_defs = filter_data.getChildByName("foomatic_defaults")
		for i in range(foo_defs.getNumChildren()):
			option = foo_defs.getChildByIndex(i);

			o_name = option.getChildByName("name").getValue()
			o_type = option.getChildByName("type").getValue()
			o_default = option.getChildByName("default").getValue()

			p_cont = p_cont + "'%s'=>{'name'=>'%s','type'=>'%s','default'=>'%s'}," % \
					(o_name, o_name, o_type, o_default)

			if o_name == 'PageSize':
				page_size = o_default

		p_cont = p_cont + '},'

	p_cont = p_cont + "'mf_type' => '%s', 'spool_dir' => '%s', 'page_size' => '%s'};" % (mf_type, spool_dir, page_size)

	perl_pipe = os.popen(printconf_dir + '/util/make_mfomatic_cfg.pl', 'w')
	debug_print(p_cont)
	perl_pipe.write(p_cont)
	return not perl_pipe.close()


# Build Print Queue
# -----------------
#
# This function takes a queue, and rebuilds it on the system. It should be rewritten as a series of co-functions for
# the different queue and filter types available in the future.
def build_print_queue(queue):
	called(build_print_queue)

	queue_name = queue.getName()
	debug_print('Rebuilding: %s' % queue_name)

	# build the appropriate queue directory, if it does not exist
	queue_dir = lpd_spool_dir + '/' + queue_name 
	if not os.access(queue_dir, os.X_OK):
		os.mkdir(queue_dir, 0700)
		os.chown(queue_dir, lp_uid, lp_gid)

	paranoid_file_write("%s/VOLATILE" % queue_dir, volatile_header, 0600, lp_uid, lp_gid)
	
	# build the name list for the printcap entry
	name_list = [queue_name]
	alias_list = queue.getChildByName('alias_list')
	for i in xrange(alias_list.getNumChildren()):
		name_list.append(alias_list.getChildByIndex(i).getValue())

	# start building the options list
	options_list = []
	options_list.extend( [	string.join(name_list, '|'),
				'sh',
				'ml=0',
				'mx=0',
				'sd=%s' % queue_dir ] )

	# Extract the real info
	queue_type = queue.getChildByName("queue_type").getValue()
	queue_data = queue.getChildByName("queue_data")
	filter_type = queue.getChildByName("filter_type").getValue()
	filter_data = queue.getChildByName("filter_data")

	# Handle the queue cases
	if queue_type == "LOCAL":
		# This is a local printer
		options_list.append("lp=%s" % queue_data.getChildByName("local_printer_device").getValue())

	elif queue_type == "LPD":
		# This is a lpd network queue
		options_list.append("rm=%s" % queue_data.getChildByName("lpd_server").getValue())

		lpd_queue = queue_data.getChildByName("lpd_queue").getValue()
		if (lpd_queue != ""):
			options_list.append("rp=%s" % lpd_queue)

		if (queue_data.getChildByName("lpd_strict_rfc1179").getValue()):
			options_list.append("bk")


	elif queue_type == "SMB":
		# This is a SMB network share
		if not os.path.exists("/usr/bin/smbclient"):
			return None

		config_str = """
share="%s"
hostip="%s"
user="%s"
password="%s"
workgroup="%s"
translate="%s"
""" % (		queue_data.getChildByName("smb_share").getValue(),
		queue_data.getChildByName("smb_ip").getValue(),
		queue_data.getChildByName("smb_user").getValue(), 
		queue_data.getChildByName("smb_password").getValue(), 
		queue_data.getChildByName("smb_workgroup").getValue(), 
		queue_data.getChildByName("smb_translate").getValue() and "yes" or "no" )

		paranoid_file_write(queue_dir + "/script.cfg", config_str, 0600, lp_uid, lp_gid)

		options_list.append("lp=|%s/smbprint" % printconf_dir)

	elif queue_type == "NCP":
		# This is a NCP network printer
		if not os.path.exists("/usr/bin/nprint"):
			return None

		config_str = """
server="%s"
queue="%s"
user="%s"
password="%s"
""" % (		queue_data.getChildByName("ncp_server").getValue(),
		queue_data.getChildByName("ncp_queue").getValue(),
		queue_data.getChildByName("ncp_user").getValue(),
		queue_data.getChildByName("ncp_password").getValue() )
		
		paranoid_file_write(queue_dir + "/script.cfg", config_str, 0600, lp_uid, lp_gid)

		options_list.append("lp=|%s/ncpprint" % printconf_dir)

	elif queue_type == "JETDIRECT":
		# This is a Jet Direct printer on the network
		config_str = """
printer_ip=%s
port=%s
""" % (		queue_data.getChildByName("jetdirect_ip").getValue(),
		queue_data.getChildByName("jetdirect_port").getValue() )

		paranoid_file_write(queue_dir + "/script.cfg", config_str, 0600, lp_uid, lp_gid)

		options_list.append("lp=|%s/jetdirectprint" % printconf_dir)

	elif 0 and queue_type == "EFAX":
		# This is an efax based fax handoff
		# EXPERIMENTAL, INCOMPLETE
		if not os.path.exists("/usr/bin/efax"):
			return None

		config_str = """
FAX_DEFAULT_PHONE_NUMBER="%s"
FAX_MODEM_DEVICE="%s"
FAX_ID_STRING="%s"
FAX_PAGE_HEADER="%s"
FAX_INIT_STRING1="%s"
FAX_INIT_STRING2="%s"
FAX_HANGUP_STRING="%s"
FAX_BIT_RATE="%s"
FAX_MINIMUM_SCAN_TIME="%s"
""" % (		queue_data.getChildByName("fax_default_phone_number").getValue(),
		queue_data.getChildByName("fax_modem_device").getValue(),
		queue_data.getChildByName("fax_id_string").getValue(),
		queue_data.getChildByName("fax_page_header").getValue(),
		queue_data.getChildByName("fax_init_string1").getValue(),
		queue_data.getChildByName("fax_init_string2").getValue(),
		queue_data.getChildByName("fax_hangup_string").getValue(),
		queue_data.getChildByName("fax_bit_rate").getValue(),
		queue_data.getChildByName("fax_minimum_scan_time").getValue() )

		paranoid_file_write(queue_dir + "/script.cfg", config_str, 0600, lp_uid, lp_gid)

		options_list.append("lp=|%s/efaxprint" % printconf_dir)

	else:	# What queue type is this? Nevermind, I'll just skip it.
		return None

	# Handle the filtration cases
	if filter_type == "NONE":
		# We are not filtering at all
		None

	elif filter_type == "MAGICFILTER":
		# We are using the magicfilter filter set
		if not magicfilter_cfg(queue_dir, filter_data):
			return None

		options_list.extend( [	"lpd_bounce=true",
					"if=%s/mf_wrapper" % printconf_dir] )

	else:	# What kind of filter is this? Hmm, I'll skip it.
		return None

	return string.join(options_list, ":\\\n\t:") + ":\n\n"


# Rebuild Queues
# --------------
#
# We are now ready to iterate over the list of remaining queues, and rebuild each one. This function does this, and
# returns the number of active queues that if found. It also destroys obsolete spool directories which it made in the
# past.
def rebuild_queues():
	called(rebuild_queues)

	# Rebuild each print queue, append their returned printcap entries to the printcap_entry_list
	printcap_entry_list = []
	active_queue_dirs = []
	for queue in queue_list:
		try:
			printcap_entry_str = build_print_queue(queue)
		except Exception, e:
			# Quietly swallow a failed queue build, and continue.
			debug_print(e.args)
			continue

		if printcap_entry_str:
			active_queue_dirs.append(queue.getName())
			printcap_entry_list.append(printcap_entry_str)

	# backup the old printcap, you know, cause we are about to zap it, and people like backups.
	if os.path.exists('/etc/printcap'):
		os.system("cp -a /etc/printcap /etc/printcap.old")

	# Write out the printcap string
	printcap = open("/etc/printcap",'w')
	printcap_local = open("/etc/printcap.local", 'r')
	printcap.write(printcap_header)
	printcap.write(string.join(printcap_entry_list))
	printcap.write(printcap_local_preamble)
	printcap.write(printcap_local.read())
	printcap.close()
	printcap_local.close()

	# This scans the spool directory, and distroys any directory marked volatile that does not have a current
	# valid configuration.
	for dir in map(lambda x: "%s/%s" % (lpd_spool_dir, x), os.listdir(lpd_spool_dir)):
		if os.path.exists("%s/VOLATILE" % dir):
			try:
				active_queue_dirs.index(os.path.basename(dir))
			except:
				# Yeah, I could do this in python, but it would suck
				os.system("rm -rf %s" % dir)


	return len(printcap_entry_list)

# We now try to rebuild. If that fails, we scan the printcap files for our return value.
if rebuild_queues() or scan_printcap_files(['/etc/printcap']):
	debug_print('Exit: Success')
	sys.exit(0)
else:
	debug_print('Exit: Failure')
	sys.exit(1)


