#! /usr/bin/python -tt
# 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 Library 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
# Copyright Red Hat Inc. 2008
#
# Author: James Antill <james.antill@redhat.com>
#
# Examples:
#
#  yum verify
#  yum verify yum*
#  yum verify all
#  yum verify extras


from yum.plugins import TYPE_INTERACTIVE
import logging # for commands
from yum import logginglevels

import time
import stat

requires_api_version = '2.5'
plugin_type = (TYPE_INTERACTIVE,)

def nevr(pkg):
    """ Identify a pkg without arch. """
    return "%s-%s:%s-%s" % (pkg.name, pkg.epoch, pkg.version, pkg.release)

def fmt_rwx(mode, r, w, x):
    ret = []
    
    if w & mode:
        ret.append("w")
    else:
        ret.append("-")
    if r & mode:
        ret.append("r")
    else:
        ret.append("-")
    if x & mode:
        ret.append("x")
    else:
        ret.append("-")

    return "".join(ret)    

def format_mode(mode):
    ret = []
    tmp = []
    if stat.S_ISUID & mode:
        tmp.append("set user (setuid)")
    if stat.S_ISGID & mode:
        tmp.append("set group (setgid)")
    if stat.S_ISVTX & mode:
        tmp.append("sticky")
    if tmp:
        ret.append("/".join(tmp))
    
    ret.append("user:"  + fmt_rwx(mode, stat.S_IRUSR,stat.S_IWUSR,stat.S_IXUSR))
    ret.append("group:" + fmt_rwx(mode, stat.S_IRGRP,stat.S_IWGRP,stat.S_IXGRP))
    ret.append("other:" + fmt_rwx(mode, stat.S_IROTH,stat.S_IWOTH,stat.S_IXOTH))
    return ", ".join(ret)

import datetime
def format_time_diff(x, y):
    frm = datetime.datetime.fromtimestamp
    if x > y:
        return str(frm(x) - frm(y)) + " later"
    else:
        return str(frm(y) - frm(x)) + " earlier"

def problem_contains(problems, types):
    for problem in problems:
        if problem.type in types:
            return problem
    return None

def pkg_multilib_file(data, pkg, pkgs, fname, verify_cb=None):

    problems = data[pkg][fname]
    ml_csum = problem_contains(problems, ['checksum'])
    ml_size = problem_contains(problems, ['size'])
    ml_time = problem_contains(problems, ['mtime'])
    if not (ml_csum or ml_size or ml_time):
        return False

    for opkg in pkgs:
        if opkg == pkg:
            continue

        if opkg not in data:
            data[opkg] = opkg.verify(callback=verify_cb)
        if fname not in data[opkg]:
            return True

        if problem_contains(problems, ['state', 'missingok', 'ghost']):
            continue
        
        problems = data[opkg][fname]
        srch = []
        if ml_csum:
            srch.append('checksum')
        if ml_size:
            srch.append('size')
        if ml_time:
            srch.append('mtime')
        if problem_contains(problems, srch):
            continue

        return True
        
    return False

# We make decisions based on these
_verify_multilib = ['mtime', 'size', 'checksum']
_verify_missingok= ['missingok', 'ghost']
_verify_none     = ['state'] + _verify_missingok
_verify_missing  = ['missing', 'permissions-missing','genchecksum']+_verify_none

# These are user configurable, for output
_verify_low      = ['mtime', 'genchecksum', 'permissions-missing'] +_verify_none
_verify_onohi    = ['mtime', 'checksum']
_verify_nnohi    = ['mtime', 'checksum']
_verify_configs  = False

class VerifyCommand:

    def __init__(self, names, conf, multilib=True, verify_configs_override=None,
                 all=False, verify_callback=None):
        self.names = names
        self.conf  = conf
        self.all = all
        self.multilib = multilib
        self.verify_configs_override = verify_configs_override
        self.verify_callback = verify_callback

    def getNames(self):
        return self.names

    def getUsage(self):
        return "[PACKAGE|all|extras]"

    def getSummary(self):
        return """\
Verify packages and display data on bad verifications"""

    def doCheck(self, base, basecmd, extcmds):
        pass

    def show_pkgs(self, msg, pkgs):
        pass

    @staticmethod
    def _filter_results(oresults, verify_none=None):
        if verify_none is None:
            verify_none = _verify_none
        results = {}
        for fn in oresults:
            probs = []
            for problem in oresults[fn]:
                if problem.type not in verify_none:
                    probs.append(problem)
            if probs:
                results[fn] = probs
        return results

    @staticmethod
    def _filter_empty(oresults):
        results = {}
        for fname in oresults:
            if oresults[fname]:
                results[fname] = oresults[fname]
        return results

    def _filter_multilib(self, data, pkg, results):
        for fname in results:
            problems = results[fname]
            mpkgs = self._multilib[nevr(pkg)]
            if not pkg_multilib_file(data, pkg, mpkgs, fname, verify_cb=self.verify_callback):
                continue

            tmp = []
            for problem in problems:
                if problem.type in _verify_multilib:
                    continue
                tmp.append(problem)
            results[fname] = tmp
        return self._filter_empty(results)

    def filter_data(self, msg, pkgs):
        data = {}
        for pkg in sorted(pkgs):
            oresults = pkg.verify(patterns=self._filename_globs, all=self.all, 
                                  callback=self.verify_callback)

            if not _verify_configs and not self.verify_configs_override:
                for fn in oresults.keys():
                    if 'configuration' in oresults[fn][0].file_types:
                        del oresults[fn]
                
            if self.multilib:
                data[pkg] = oresults
            else:
                if self.all:
                    results = self._filter_results(oresults, _verify_missingok)
                else:
                    results = self._filter_results(oresults)
                if results:
                    yield (pkg, results)
            
        if not self.multilib:
            return

        ndata = {}
        for pkg in data:
            results = self._filter_results(data[pkg])
            if nevr(pkg) in self._multilib:
                ndata[pkg] = self._filter_multilib(data, pkg, results)
            else:
                ndata[pkg] = results
        
        for pkg in sorted(pkgs):
            if pkg in ndata and ndata[pkg]:
                yield (pkg, ndata[pkg])

    def _mode_except(self, base, line, problem=None, exceptions=None):
        if exceptions is None:
            exceptions = _verify_low
        if problem is not None and problem.type in exceptions:
            return ("", "")
        
        hib = ""
        hie = ""

        name = 'fg_' + line
        if name in self.conf and self.conf[name] in base.term.FG_COLOR:
            hib += base.term.FG_COLOR[self.conf[name]]
        name = 'bg_' + line
        if name in self.conf and self.conf[name] in base.term.BG_COLOR:
            hib += base.term.BG_COLOR[self.conf[name]]
        name = 'hi_' + line
        if name in self.conf and self.conf[name] in base.term.MODE:
            hib += base.term.MODE[self.conf[name]]
            
        hie = base.term.MODE['normal']
        return (hib, hie)

    def show_problem(self, base, msg, problem, done):
        if done:
            msg("%s%s%s" % (' ' * 35, '-' * 8, ' ' * 35))
        (hib, hie) = self._mode_except(base, 'prob', problem)
        msg("        Problem:  " + hib + problem.message + hie)
        if problem.type not in _verify_missing:
            cv = problem.disk_value
            ov = problem.database_value
            if problem.type == 'mtime':
                cv = time.ctime(cv) + " (%s)" % format_time_diff(cv, ov)
                ov = time.ctime(ov)
            if problem.type == 'mode':
                cv = format_mode(cv)
                ov = format_mode(ov)
            if problem.type == 'size':
                cv = "%*s" % (5, base.format_number(cv))
                ov = "%*s" % (5, base.format_number(ov))
                if cv == ov: # ignore human units, so we can see the diff.
                    cv = "%*s B" % (12, str(problem.disk_value))
                    ov = "%*s B" % (12, str(problem.database_value))

            (hib, hie) = self._mode_except(base, 'new', problem, _verify_nnohi)
            msg("        Current:  " + hib + cv + hie)
            (hib, hie) = self._mode_except(base, 'old', problem, _verify_onohi)
            msg("        Original: " + hib + ov + hie)
        
    def show_data(self, base, msg, pkgs, name):
        done = False
        problem_found = False
        mcb = lambda x: base.matchcallback(x, [])
        for (pkg, results) in self.filter_data(msg, pkgs):
            if not done:
                msg("%s %s %s" % ('=' * 20, name, '=' * 20))
            else:
                msg('')
            done = True
            
            mcb(pkg)
            for fname in sorted(results):
                hiprobs = len(filter(lambda x: x.type not in _verify_low,
                                     results[fname]))
                if hiprobs:
                    (hib, hie) = self._mode_except(base, 'file')
                else:
                    (hib, hie) = ("", "")
                msg("    File: " + hib + fname + hie)
                if hiprobs:
                    (hib, hie) = self._mode_except(base, 'tags')
                else:
                    (hib, hie) = ("", "")
                done_prob = False
                for problem in sorted(results[fname]):
                    problem_found = True
                    if not done_prob and problem.file_types:
                        tags = ", ".join(problem.file_types)
                        msg("    Tags: " + hib + tags + hie)
                    self.show_problem(base, msg, problem, done_prob)
                    done_prob = True
        return problem_found

    def doCommand(self, base, basecmd, extcmds):
        global _verify_configs

        logger = logging.getLogger("yum.verbose.main")
        def msg(x):
            logger.log(logginglevels.INFO_2, x)
        def msg_warn(x):
            logger.warn(x)

        opts = base.plugins.cmdline[0]
        if opts.verify_configuration_files is not None:
            val = opts.verify_configuration_files
            if False: pass
            elif val.lower() in ["0", "no", "false", "off"]:
                _verify_configs = False
            elif val.lower() in ["1", "yes", "true", "on"]:
                _verify_configs = True
            else:
                msg_warn("Ignoring bad value \"%s\" for the option %s" %
                         (val, "--verify-configuration-files"))
        self._filename_globs = None
        if opts.verify_filenames:
            self._filename_globs = opts.verify_filenames
            
        subgroup = ["installed"]
        if len(extcmds):
            if extcmds[0] == "all":
                extcmds = extcmds[1:]
            elif extcmds[0] == "extras":
                subgroup = ["extras"]
                extcmds = extcmds[1:]

        if self.multilib:
            pkgs = base.returnPkgLists(["installed"]).installed
            self._multilib = {}
            for pkg in pkgs:
                self._multilib.setdefault(nevr(pkg), []).append(pkg)
            for pkg in pkgs:
                if len(self._multilib[nevr(pkg)]) == 1:
                    del self._multilib[nevr(pkg)]
            # self._multilib is now a dict of all pkgs that have more than one
            # nevr() match
            
        ypl = base.returnPkgLists(subgroup + extcmds)
        result = (self.show_data(base, msg, ypl.installed, 'Installed Packages')
                  or self.show_data(base, msg, ypl.extras,    'Extra Packages'))

        return result, [basecmd + ' done']

    def needTs(self, base, basecmd, extcmds):
        if not len(extcmds) or extcmds[0] != 'extras':
            return False
        
        return True


def config_hook(conduit):
    '''
    Yum Plugin Config Hook: 
    Add the 'verify' and 'verify-no-multilib' commands.
    '''
    global _verify_configs
    global _verify_low
    global _verify_onohi
    global _verify_nnohi

    _verify_configs = conduit.confBool('main', 'configuration-files',
                                       default=False)
    
    low = conduit.confString('highlight', 'low-priority', default=None)
    if low:
        _verify_low   = filter(len, low.replace(',', ' ').split())
    fold = conduit.confString('highlight', 'filter-old', default=None)
    if fold:
        _verify_onohi = filter(len, fold.replace(',', ' ').split())
    fnew = conduit.confString('highlight', 'filter-new', default=None)
    if fnew:
        _verify_nnohi = filter(len, fnew.replace(',', ' ').split())

    conf = {}

    conf['hi_prob'] = conduit.confString('highlight', 'problem', default='bold')
    conf['fg_prob'] = conduit.confString('highlight', 'problem-fg',default=None)
    conf['bg_prob'] = conduit.confString('highlight', 'problem-bg',default=None)
    
    conf['hi_new'] = conduit.confString('highlight', 'new', default='reverse')
    conf['fg_new'] = conduit.confString('highlight', 'new-fg', default=None)
    conf['bg_new'] = conduit.confString('highlight', 'new-bg', default=None)
    
    conf['hi_old'] = conduit.confString('highlight', 'old',    default=None)
    conf['fg_old'] = conduit.confString('highlight', 'old-fg', default='red')
    conf['bg_old'] = conduit.confString('highlight', 'old-bg', default=None)
    
    conf['hi_file'] = conduit.confString('highlight', 'file',
                                         default='underline')
    conf['fg_file'] = conduit.confString('highlight', 'file-fg',
                                         default='green')
    conf['bg_file'] = conduit.confString('highlight', 'file-bg', default=None)
    
    conf['hi_tags'] = conduit.confString('highlight', 'tags',    default='bold')
    conf['fg_tags'] = conduit.confString('highlight', 'tags-fg',
                                         default='yellow')
    conf['bg_tags'] = conduit.confString('highlight', 'tags-bg',
                                         default='black')

    reg = conduit.registerCommand
    reg(VerifyCommand(['verify-all'], conf, multilib=False,
                      verify_configs_override=True, all=True, 
                      verify_callback=conduit._base.verify_plugins_cb))
    reg(VerifyCommand(['verify-rpm'], conf, multilib=False,
                      verify_configs_override=True,
                      verify_callback=conduit._base.verify_plugins_cb))
    reg(VerifyCommand(['verify-multilib','verify'], conf,
                      verify_callback=conduit._base.verify_plugins_cb))

    parser = conduit.getOptParser()
    if not parser:
        return

    def make_nopt(attrs):
        attrs = attrs.replace("-", "_")
        def func(opt, key, val, parser):
            vals = str(val).replace(",", " ").split()
            vals = filter(len, vals)
            getattr(parser.values, 'verify_' + attrs).extend(vals)
        return func

    if hasattr(parser, 'plugin_option_group'):
        parser = parser.plugin_option_group
    parser.add_option('--verify-filenames', action="callback",
                      callback=make_nopt('filenames'), default=[],type="string",
                      help='Only verify files matching this')

    parser.add_option('--verify-configuration-files', action="store",
                      type="string",
                      help='Enable/disable verification of files tagged as configuration files')
