#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Univention ucslint
"""Check UCS packages for policy compliance."""
#
# Copyright 2008-2014 Univention GmbH
#
# http://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <http://www.gnu.org/licenses/>.
#
#
import fnmatch
import os
import sys
import imp
import optparse
import re
try:
	import univention.ucslint.base as uub
except ImportError:
	try:
		import ucslint.base as uub
		print >> sys.stderr, 'using fallback ucslint.base instead of univention.ucslint.base'
	except ImportError:
		sys.path.insert(0, os.path.curdir)
		import ucslint.base as uub
		print >> sys.stderr, 'using local ucslint.base instead of univention.ucslint.base'


class DebianPackageCheck(object):
	"""Check Debian package for policy compliance."""
	def __init__(self, path, plugindirs, enabled_modules=None, disabled_modules=None, debuglevel=0):
		self.path = path
		self.plugindirs = plugindirs
		self.pluginlist = {}
		self.msglist = []
		self.enabled_modules = enabled_modules
		self.disabled_modules = disabled_modules
		self.debuglevel = debuglevel
		self.msgidlist = {}
		self.overrides = set()
		self.loadplugins()


	def loadplugins(self):
		"""Load modules from plugin directory."""
		for plugindir in plugindirs:
			plugindir = os.path.expanduser(plugindir)
			if not os.path.exists(plugindir):
				if self.debuglevel:
					print >> sys.stderr, 'WARNING: plugindir %s does not exist' % plugindir
			else:
				for f in os.listdir( plugindir ):
					if f.endswith('.py') and f[0:4].isdigit():
						# self.modules == None ==> load all modules
						# otherwise load only listed modules
						if ( not self.enabled_modules or f[0:4] in self.enabled_modules ) and not f[0:4] in self.disabled_modules:
							modname = f[0:-3]
							fd = open( os.path.join( plugindir, f ) )
							module = imp.new_module(modname)
							try:
								exec fd in module.__dict__
								self.pluginlist[modname] = module
								if self.debuglevel:
									print >> sys.stderr, 'Loaded module %s' % modname
							except Exception:
								print >> sys.stderr, 'ERROR: Loading module %s failed' % f
								if self.debuglevel:
									raise
						else:
							if self.debuglevel:
								print >> sys.stderr, 'Module %s is not enabled' % f


	def check(self):
		"""Run plugin on files in path."""
		for plugin in self.pluginlist.values():
			obj = plugin.UniventionPackageCheck()
			self.msgidlist.update( obj.getMsgIds() )
			obj.setdebug( self.debuglevel )
			obj.postinit( self.path )
			try:
				obj.check( self.path )
			except uub.UCSLintException, ex:
				print >> sys.stderr, ex
			self.msglist.extend( obj.result() )


	def modifyMsgIdList(self, newmap):
		"""Set severity level of messages.
		newmap == { RESULT_WARN: [ '0004-1', '0019-17', ... ],
					RESULT_ERROR: [ '0004-2' ],
					}
		"""
		for level, idlist in newmap.items():
			for curid in idlist:
				if curid in self.msgidlist:
					self.msgidlist[ curid ][0] = level


	def loadOverrides(self):
		"""Parse debian/ucslint.overrides file.
		"""
		self.overrides = set()
		fn = os.path.join( self.path, 'debian', 'ucslint.overrides' )
		if os.path.isfile(fn):
			lines = []
			try:
				lines = open(fn, 'r').readlines()
			except IOError:
				print >> sys.stderr, 'WARNING: cannot load debian/ucslint.overrides'

			reModule = re.compile('^(\d+-\d+):?\s*')
			reFilename = re.compile('^(.*?)(:\d+)?$')
			for line in lines:
				module = None
				filename = None
				linenumber = None

				line = line.strip()

				# find UCR module and cut it off
				result = reModule.search(line)
				if not result:
					continue
				module = result.group(1)
				line = line[ result.end(): ]

				# find filename and cut it off
				result = reFilename.search(line)
				if not result:
					self.overrides.add( (module, filename, linenumber, ) )
					continue
				filename = result.group(1)
				if not filename:
					filename = None
				linenumber = result.group(2)
				if linenumber:
					linenumber = int(linenumber.strip(':'))

				self.overrides.add( (module, filename, linenumber, ) )

	def inOverrides(self, module, filename, linenumber):
		"""Check message agains overrides."""
		if (module, None, None, ) in self.overrides:
			return True
		for (modulename, pattern, number, ) in self.overrides:
			if modulename == module:
				if fnmatch.fnmatch(os.path.abspath(filename), os.path.abspath(pattern)):
					if number is None or number == linenumber:
						return True
		return False

	def printResult(self, ignore_IDs, display_only_IDs, display_only_categories, exitcode_categories ):
		"""Print result of cheks."""
		incident_cnt = 0
		exitcode_cnt = 0

		self.loadOverrides()

		for msg in self.msglist:
			if msg.getId() in ignore_IDs:
				continue
			if display_only_IDs and not msg.getId() in display_only_IDs:
				continue
			if self.inOverrides( msg.getId(), msg.filename, msg.line ):
				# ignore msg if mentioned in overrides files
				continue
			category = uub.RESULT_INT2STR.get( self.msgidlist.get( msg.getId(), ['FIXME'] )[0], 'FIXME')
			if category in display_only_categories or display_only_categories == '':
				print '%s:%s' % (category , str(msg))
				incident_cnt += 1

				if category in exitcode_categories or exitcode_categories == '':
					exitcode_cnt += 1

		return incident_cnt, exitcode_cnt


def clean_id(idstr):
	"""Format ID strng."""
	if not '-' in idstr:
		raise ValueError('no valid id (%s) - missing dash' % idstr)
	modid, msgid = idstr.strip().split('-', 1)
	return '%s-%s' % (clean_modid(modid), clean_msgid(msgid))


def clean_modid(modid):
	"""Format module ID string."""
	if not modid.isdigit():
		raise ValueError('modid contains invalid characters: %s' % modid)
	return '%04d' % (int(modid))


def clean_msgid(msgid):
	"""Format message ID string."""
	if not msgid.isdigit():
		raise ValueError('msgid contains invalid characters: %s' % msgid)
	return '%d' % int(msgid)


if __name__ == '__main__':
	usage = "usage: %prog [options] [<path>]"
	parser = optparse.OptionParser(usage=usage)
	parser.add_option( '-d', '--debug', action = 'store', type = 'int',
					   dest = 'debug', default = 0,
					   help = 'if set, debugging is activated and set to the specified level' )

	parser.add_option( '-m', '--modules', action = 'store', type = 'string',
					   dest = 'enabled_modules', default = '',
					   help = 'list of modules to be loaded (e.g. -m 0009,27)' )

	parser.add_option( '-x', '--exclude-modules', action = 'store', type = 'string',
					   dest = 'disabled_modules', default = '',
					   help = 'list of modules to be disabled (e.g. -x 9,027)' )

	parser.add_option( '-o', '--display-only', action = 'store', type = 'string',
					   dest = 'display_only_IDs', default = '',
					   help = 'list of IDs to be displayed (e.g. -o 9-1,0027-12)' )

	parser.add_option( '-i', '--ignore', action = 'store', type = 'string',
					   dest = 'ignore_IDs', default = '',
					   help = 'list of IDs to be ignored (e.g. -i 0003-4,19-27)' )

	parser.add_option( '-p', '--plugindir', action = 'append', type = 'string',
					   dest = 'plugindir', default = [],
					   help = 'override plugin directory with <plugindir>' )

	parser.add_option( '-c', '--display-categories', action = 'store', type = 'string',
					   dest = 'display_only_categories', default = '',
					   help = 'categories to be displayed (e.g. -c EWIS)' )

	parser.add_option( '-e', '--exitcode-categories', action = 'store', type = 'string',
					   dest = 'exitcode_categories', default = 'E',
					   help = 'categories that cause an exitcode != 0 (e.g. -e EWIS)' )

	( options, args ) = parser.parse_args()

	pkgpath = '.'
	if len(args) > 0:
		pkgpath = args[0]

	if not os.path.exists( pkgpath ):
		parser.error("directory %s does not exist!" % pkgpath)

	if not os.path.isdir( pkgpath ):
		parser.error("%s is no directory!" % pkgpath)

	if not os.path.isdir( os.path.join(pkgpath, 'debian') ):
		parser.error("%s/debian does not exist or is not a directory!" % pkgpath)

	plugindirs = [
		'~/.ucslint',
		os.path.dirname(uub.__file__),
		]

	# override plugin directories
	if options.plugindir:
		plugindirs = options.plugindir

	if options.ignore_IDs:
		options.ignore_IDs = options.ignore_IDs.split(',')
		options.ignore_IDs = [ clean_id(x) for x in options.ignore_IDs ]

	if options.display_only_IDs:
		options.display_only_IDs = options.display_only_IDs.split(',')
		options.display_only_IDs = [ clean_id(x) for x in options.display_only_IDs ]

	if options.enabled_modules:
		options.enabled_modules = options.enabled_modules.split(',')
		options.enabled_modules = [ clean_modid(x) for x in options.enabled_modules ]
	else:
		options.enabled_modules = []

	if options.disabled_modules:
		options.disabled_modules = options.disabled_modules.split(',')
		options.disabled_modules = [ clean_modid(x) for x in options.disabled_modules ]
	else:
		options.disabled_modules = []

	chk = DebianPackageCheck( pkgpath, plugindirs, enabled_modules=options.enabled_modules, disabled_modules=options.disabled_modules, debuglevel=options.debug )
	try:
		chk.check()
	except uub.UCSLintException, ex:
		print >> sys.stderr, ex
	incident_cnt, exitcode_cnt = chk.printResult( options.ignore_IDs, options.display_only_IDs, options.display_only_categories, options.exitcode_categories )

	if exitcode_cnt:
		sys.exit(1)
