#
# Reportbug module - common functions for reportbug and greportbug
#   Written by Chris Lawrence <lawrencc@debian.org>
#   (C) 1999-2000 Chris Lawrence
#
# This program is freely distributable per the following license:
#
##  Permission to use, copy, modify, and distribute this software and its
##  documentation for any purpose and without fee is hereby granted,
##  provided that the above copyright notice appears in all copies and that
##  both that copyright notice and this permission notice appear in
##  supporting documentation.
##
##  I DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
##  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL I
##  BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
##  DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
##  WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
##  ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
##  SOFTWARE.
#
# Version ##VERSION##; see changelog for revision history

import time, sys, string, os, locale, re, pwd, commands, shlex

# Severity levels
SEVERITIES = {'c':'critical',
              'g':'grave',
              's':'serious',
              'i':'important',
              'n':'normal',
              'm':'minor',
              'f':'fixed',
              'w':'wishlist'}

# Rank order of severities, for sorting
SEVLIST = ['critical', 'grave', 'serious', 'important', 'normal', 'minor',
           'wishlist', 'fixed']

# These packages are virtual in Debian; we don't look them up...
debother = {
    'base' : 'General bugs in the base system',
    'boot-floppies' : 'Bugs in the boot and root disks',
    'bugs.debian.org' : 'The bug tracking system, @bugs.debian.org',
    'ftp.debian.org' : 'Problems with the main FTP site (or mirrors)',
    'nonus.debian.org' : 'Problems with the non-us FTP site (or mirrors)',
    'www.debian.org' : 'Problems with the website (or mirrors)',
    'manual' : 'Bugs in the manual',
    'project' : 'Problems related to Project administration',
    'general' : 'Widespread problems (e.g., that many manpages are mode 755)',
    'kernel' : 'Problems with the kernel in general (otherwise: kernel-image)',
    'lists.debian.org' : 'The mailing lists (debian-*@lists.debian.org)',
    'listarchives' : 'The mailing list archives.' }

# Supported servers
# Theoretically support for GNATS and Jitterbug could be added here.

SYSTEMS = { 'debian' :
            { 'name' : 'Debian', 'email': '%s@bugs.debian.org',
              'btsroot' : 'http://www.debian.org/Bugs/',
              'query-dpkg' : 1, 'type' : 'debbugs',
              'otherpkgs' : debother,
              'ldap' : ('bugs.debian.org', 35567,
                        'ou=Bugs,o=Debian Project,c=US'),
              'mirrors' :
              { 'us' : 'http://www.debian.org/Bugs/',
                'uk' : 'http://www.chiark.greenend.org.uk/debian/Bugs/',
                'nl' : 'http://www.nl.debian.org/Bugs/',
                'de' : 'http://www.infodrom.north.de/Debian/Bugs/',
                }
              },
            'tdyc' :
            { 'name' : 'TDYC [Debian KDE]',
              'email': '%s@bugs.tdyc.com',
              'btsroot' : 'http://bugs.tdyc.com/',
              'otherpkgs' : {}, 'type' : 'debbugs',
              'namefmt' : '%s-debian', 'query-dpkg' : 1,
              'mirrors' : {} },
            'kde' :
            { 'name' : 'KDE Project', 'email': '%s@bugs.kde.org',
              'btsroot': 'http://bugs.kde.org/', 'type' : 'debbugs',
              'query-dpkg' : 0, 'otherpkgs' : {},
              'mirrors' : {} },
            'mandrake' :
            { 'name' : 'Mandrake Linux', 'email': '%s@bugs.linux-mandrake.com',
              'btsroot': 'http://www.linux-mandrake.com/bugs/',
              'type' : 'debbugs', 'query-dpkg' : 0, 'otherpkgs' : {},
              'mirrors' : {} },
            'gnome' :
            { 'name' : 'GNOME Project', 'email': '%s@bugs.gnome.org',
              'btsroot': 'http://bugs.gnome.org/',
              'type' : 'debbugs', 'mirrors' : {},
              'query-dpkg' : 0, 'otherpkgs' : {} },
            }

# Convenient aliases
SYSTEMS['kde-debian'] = SYSTEMS['tdyc']

PSEUDOHEADERS = ('Package', 'Version', 'Severity', 'File')

def rfcdatestr(timeval = None):
    """Cheesy implementation of an RFC 822 date."""
    if timeval is None:
        timeval = time.time()

    try:
        tm = time.localtime(timeval)
        if tm[8] > 0:
            hrs, mins = divmod(-time.altzone/60, 60)
        else:
            hrs, mins = divmod(-time.timezone/60, 60)

        # Correct for rounding down
        if mins and hrs < 0:
            hrs = hrs + 1

        locale.setlocale(locale.LC_TIME, "C")
        return time.strftime('%a, %d %b %Y %H:%M:%S', tm) + (" %+03d%02d" %
                                                             (hrs, mins))
    except:
        return 'Invalid date: '+`timeval`

fhs_directories = ['/', '/usr', '/usr/share', '/var', '/usr/X11R6',
                   '/usr/man', '/usr/doc', '/usr/bin']

def realpath(filename):
    filename = os.path.abspath(filename)

    bits = string.split(filename, '/')
    for i in range(2, len(bits)+1):
        component = string.join(bits[0:i], '/')
        if component in fhs_directories:
            continue
        
        if os.path.islink(component):
            resolved = os.readlink(component)
            (dir, file) = os.path.split(component)
            resolved = os.path.normpath(os.path.join(dir, resolved))
            newpath = apply(os.path.join, [resolved] + bits[i:])
            return realpath(newpath)

    return filename

pathdirs = ['/usr/sbin', '/usr/bin', '/sbin', 'bin']

def search_path_for(filename):
    dir, file = os.path.split(filename)
    if dir: return realpath(filename)
    
    path = string.split(os.environ.get("PATH", os.defpath), os.pathsep)
    for dir in pathdirs:
        if not dir in path:
            path.append(dir)
    
    for dir in path:
        fullname = os.path.join(dir, filename)
        if os.path.exists(fullname):
            return realpath(fullname)
    return None

def find_package_for(filename):
    """Find the package(s) containing this file."""
    packages = {}
    newfilename = search_path_for(filename)
    if newfilename:
        filename = newfilename
        
    pipe = os.popen("dpkg --search '%s'" % filename)
    matcher = re.compile('diversion by')

    for line in pipe.readlines():
        line = string.strip(line)
        # Ignore diversions
        if matcher.match(line): continue

        (package, path) = string.split(line, ':')
        packlist = string.split(package, ', ')
        for package in packlist:
            if packages.has_key(package):
                packages[package].append(path)
            else:
                packages[package] = [path]
    pipe.close()
    return filename, packages

def quote_if_needed(realname):
    acceptable = string.letters+string.digits+' '
    quote = 0
    for char in realname:
        if char not in acceptable:
            quote = 1
    
    if quote:
        return '"%s"' % realname
    else:
        return realname
        
def get_user_id(email='', realname=''):
    uid = os.getuid()
    info = pwd.getpwuid(uid)
    email = os.environ.get('DEBEMAIL') or os.environ.get('EMAIL') or email
    if not email:
        if not os.path.exists('/etc/mailname'):
            sys.stderr.write('Please set the environment variable DEBEMAIL '
                             'or EMAIL and try again.\n')
            sys.exit(1)

        fp = open('/etc/mailname')
        domainname = string.strip(fp.readline())
        fp.close()

        email = info[0]+'@'+domainname

    # Handle EMAIL if it's formatted as 'Bob <bob@host>'.
    if '<' in email:
        return email
        
    realname = realname or string.split(info[4], ',')[0]
    realname = os.environ.get('DEBNAME') or os.environ.get('NAME') or \
               os.environ.get('USERNAME') or realname

    realname = quote_if_needed(realname)
    return '%s <%s>' % (realname, email)
    
def get_package_status(package):
    versionre = re.compile('Version: ')
    packagere = re.compile('Package: ')
    dependsre = re.compile('(Pre-)?Depends: ')
    conffilesre = re.compile('Conffiles:')
    maintre = re.compile('Maintainer:')

    pkgversion = pkgavail = depends = maintainer = None
    conffiles = []
    
    confmode = 0
    pipe = os.popen("dpkg --status '%s'" % package)
    for line in pipe.readlines():
        line = string.strip(line)
        if not line: continue

        if confmode:
            if line[0] != '/':
                confmode = 0
            else:
                conffiles.append(string.split(line))

        if versionre.match(line):
            (crud, pkgversion) = string.split(line, ": ")
        elif packagere.match(line):
            (crud, pkgavail) = string.split(line, ": ")
        elif dependsre.match(line):
            (crud, thisdepends) = string.split(line, ": ")
            if depends:
                depends = depends+', '+thisdepends
            else:
                depends = thisdepends
        elif conffilesre.match(line):
            confmode = 1
        elif maintre.match(line):
            crud, maintainer = string.split(line, ": ")

    pipe.close()

    return (pkgversion, pkgavail, depends, conffiles, maintainer)

def get_package_info(packages):
    # Package: package | Provides: package
    fp = open('/var/lib/dpkg/status')
    dbase = fp.read()
    fp.close()

    pkgname = r'(?:[\S]+(?:$|,\s+))'

    groupfor = {}
    searchbits = []
    for (group, package) in packages:
        groupfor[package] = group
        searchbits.append(r'^(?P<hdr>Package):\s+'+re.escape(package)+'$')
        searchbits.append(r'^(?P<hdr>Provides):\s+'+pkgname+
                          r'*(?P<pkg>'+re.escape(package)+r')(?:$|,\s+)'+
                          pkgname+'*')

    groups = groupfor.values()
    found = {}
    for group in groups: found[group] = 0
    
    searchobs = map(lambda x: re.compile(x, re.MULTILINE), searchbits)
    packob = re.compile('^Package: (?P<pkg>.*)$', re.MULTILINE)
    statob = re.compile('^Status: (?P<stat>.*)$', re.MULTILINE)
    versob = re.compile('^Version: (?P<vers>.*)$', re.MULTILINE)
    descob = re.compile('^Description: (?P<desc>.*)$', re.MULTILINE)

    ret = []
    packinfo = string.split(dbase, '\n\n')
    for p in packinfo:
        for ob in searchobs:
            m = ob.search(p)
            if m:
                pack = packob.search(p).group('pkg')
                stat = statob.search(p).group('stat')
                sinfo = string.split(stat)
                stat = sinfo[0][0] + sinfo[2][0]
                if stat[1] != 'i':
                    continue

                if m.group('hdr') == 'Provides':
                    provides = m.group('pkg')
                else:
                    provides = None
                
                vers = versob.search(p).group('vers')
                desc = descob.search(p).group('desc')

                info = (pack,stat,vers,desc,provides)
                ret.append(info)
                group = groupfor.get(pack, groupfor.get(provides))
                found[group] = 1

    for group in groups:
        if not found[group]:
            ret.append( (group, 'pn', '', 'Not found.', None) )

    return ret

def get_dependency_info(package, depends):
    depends = string.split(depends, ', ')
    dependencies = []
    packre = re.compile(r'(\S+)(?:\s+\([^)]+\))?')
    for dep in depends:
        bits = re.split('\s*\|\s*', dep)
        for bit in bits:
            m = packre.match(bit)
            if m: dependencies.append( (dep, m.group(1)) )

    depinfo = "\nVersions of packages %s depends on:\n" % package

    deplist = []
    for info in get_package_info(dependencies):
        if info not in deplist:
            deplist.append(info)

    maxlen = max(map(lambda x: len(x[2]), deplist) + [10])
    deplist.sort()
    for (pack, status, vers, desc, provides) in deplist:
        if provides:
            pack = pack + ' [' + provides + ']'
        
        packstuff = '%-*.*s %s' % (39-maxlen, 39-maxlen, pack, vers)
                
        info = '%-3.3s %-40.40s %-34.34s\n' % (status, packstuff, desc)
        depinfo = depinfo + info
    
    return depinfo

def old_get_dependency_info(package, depends):
    dependencies = string.split(depends, ', ')
    dependencies = map(lambda x: string.split(x, ' ')[0], dependencies)

    pipe = os.popen("dpkg -l "+string.join(dependencies)+" | uniq")
    pkgre = re.compile('[uirp]')
    depinfo = "\nVersions of packages %s depends on:\n" % package
    for line in pipe.readlines():
        if not pkgre.match(line): continue
        depinfo = depinfo + line
    pipe.close()
    
    return depinfo

def get_changed_config_files(conffiles, nocompress=0):
    confinfo = {}
    changed = []
    for (file, md5sum) in conffiles:
        try:
            fp = open(file)
        except IOError, msg:
            confinfo[file] = msg
            continue

        filemd5 = string.split(commands.getoutput('md5sum ' + file))[0]
        if filemd5 == md5sum: continue

        changed.append(file)
        thisinfo = 'changed:\n'
        for line in fp.readlines():
            if not line: continue

            if line == '\n' and not nocompress: continue
            if line[0] == '#' and not nocompress: continue

            thisinfo = thisinfo + line

        confinfo[file] = thisinfo

    return confinfo, changed

def generate_blank_report(package, pkgversion, severity, depinfo, confinfo,
                          foundfile='', incfiles='', system='debian'):
    if SYSTEMS[system]['query-dpkg']:
        debvers = ''
        if os.path.exists('/etc/debian_version'):
            file = open('/etc/debian_version')
            debvers = string.strip(file.readline())
            file.close()

        debarch = commands.getoutput('dpkg --print-installation-architecture')
        debinfo = "Debian Release: %s\nArchitecture: %s\n" % (debvers, debarch)
    else:
        debinfo = ''

    if foundfile:
        fileinfo = 'File: %s\n' % foundfile
    else:
        fileinfo = ''

    if SYSTEMS[system].has_key('namefmt'):
        package = SYSTEMS[system]['namefmt'] % package

    if severity:
        severity = 'Severity: %s\n' % severity

    specials = ''
##    specialpath = '/usr/share/doc/%s/Debian.bugtemplate' % package
##    if os.path.exists(specialpath) and os.path.isfile(specialpath):
##        specials = open(specialpath).read()
##        if specials[-1] != '\n':
##            specials = specials + '\n'

##        specials = '-- Special Instructions\n' + specials + '\n'

    report = """Package: %s
Version: %s
%s%s
%s

%s-- System Information
%sKernel: %s
%s%s""" % (package, pkgversion, severity, fileinfo, incfiles, specials,
           debinfo, string.join(os.uname()), depinfo, confinfo)

    return report

class our_lex(shlex.shlex):
    def get_token(self):
        token = shlex.shlex.get_token(self)
        if not len(token): return token
        if (token[0] == token[-1]) and token[0] in self.quotes:
            token = token[1:-1]
        return token

FILES = ('/etc/reportbug.conf', '~/.reportbugrc')

def parse_config_files():
    args = {}
    for file in FILES:
        file = os.path.expanduser(file)
        if os.path.exists(file):
            try:
                lex = our_lex(open(file))
            except IOError, msg:
                continue
            
            lex.wordchars = lex.wordchars + '-.@/:<>'

            token = lex.get_token()
            while token:
                token = string.lower(token)
                if token in ('quiet', 'maintonly', 'submit'):
                    args['sendto'] = token
                elif token == 'severity':
                    token = string.lower(lex.get_token())
                    if token in SEVERITIES.values():
                        args['severity'] = token
                elif token == 'mua':
                    args['mua'] = lex.get_token()
                elif token == 'mutt':
                    args['mua'] = 'mutt -H'
                elif token == 'af':
                    args['mua'] = 'af -EH <'
                elif token in ('mh', 'nmh'):
                    args['mua'] = '/usr/bin/mh/comp -draftmessage'
                elif token == 'header':
                    args['headers'] = args.get('headers', []) + \
                                      [lex.get_token()]
                elif token == 'no-cc':
                    args['nocc'] = 1
                elif token == 'cc':
                    args['nocc'] = 0
                elif token == 'no-compress':
                    args['nocompress'] = 1
                elif token == 'compress':
                    args['nocompress'] = 0
                elif token == 'no-bts-query':
                    args['dontquery'] = 1
                elif token == 'bts-query':
                    args['dontquery'] = 0
                elif token == 'config-files':
                    args['noconf'] = 0
                elif token == 'no-config-files':
                    args['noconf'] = 1
                elif token == 'ldap':
                    args['ldap'] = 1
                elif token == 'no-ldap':
                    args['ldap'] = 0
                elif token == 'email':
                    args['email'] = lex.get_token()
                elif token == 'realname':
                    args['realname'] = lex.get_token()
                elif token == 'replyto':
                    args['replyto'] = lex.get_token()
                elif token == 'http_proxy':
                    args['http_proxy'] = lex.get_token()
                elif token == 'smtphost':
                    args['smtphost'] = lex.get_token()
                elif token == 'sign':
                    token = string.lower(lex.get_token())
                    if token in ('pgp', 'gpg'):
                        args['sign'] = token
                    elif token == 'gnupg':
                        args['sign'] = 'gpg'
                    elif token == 'none':
                        args['sign'] = ''
                elif token == 'bts':
                    token = string.lower(lex.get_token())
                    if token in SYSTEMS.keys():
                        args['bts'] = token
                elif token == 'mirror':
                    args['mirrors'] = args.get('mirrors', []) + \
                                      [lex.get_token()]
                else:
                    sys.stderr.write('Unrecognized token: '+token+'\n')

                token = lex.get_token()

    return args
