#
# Reportbug module - common functions for reportbug and greportbug
#   Written by Chris Lawrence <lawrencc@debian.org>
#   (C) 1999-2002 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, debianbts
import rfc822, socket

# Headers other than these become email headers for debbugs servers
PSEUDOHEADERS = ('Package', 'Version', 'Severity', 'File', 'Tags',
                 'Justification', 'Followup-For')

#VALID_UIS = ('newt', 'text')
VALID_UIS = ('text',)

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

        current = locale.setlocale(locale.LC_TIME)
        locale.setlocale(locale.LC_TIME, "C")
        ret = time.strftime('%a, %d %b %Y %H:%M:%S', tm) + (" %+03d%02d" %
                                                            (hrs, mins))
        locale.setlocale(locale.LC_TIME, current)
        return ret
    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', '/usr/X11R6/bin',
            '/usr/games']

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 query_dpkg_for(filename):
    try:
        x = os.getcwd()
    except OSError:
        os.chdir('/')
    pipe = os.popen("COLUMNS=79 dpkg --search '%s' 2>/dev/null" % filename)
    matcher = re.compile('diversion by')
    packages = {}

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

        (package, path) = string.split(line, ':', 1)
        path = string.strip(path)
        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 find_package_for(filename):
    """Find the package(s) containing this file."""
    packages = {}
    if filename[0] == '/':
        file, pkglist = query_dpkg_for(filename)
        if pkglist: return file, pkglist
        
    newfilename = search_path_for(filename)
    return query_dpkg_for(newfilename or filename)

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 '"'+realname+'"'
    return realname

def find_rewritten(username):
    for filename in ['/etc/email-addresses']:
        if os.path.exists(filename):
             fp = open(filename)
             for line in fp.readlines():
                 line = string.strip(line)
                 line = string.split(line, '#')[0]
                 if not line:
                     continue
                 try:
                     name, alias = string.split(line, ':')
                     if string.strip(name) == username:
                         return string.strip(alias)
                 except ValueError:
                     print 'Invalid entry in %s' % filename
                     return None
        
# Swiped directly from Python 2.2 CVS. (C) 2001 Python Software Foundation
# See /usr/share/doc/python2.{1,2}/copyright for license.
def getfqdn(name=''):
    """Get fully qualified domain name from name.

    An empty argument is interpreted as meaning the local host.

    First the hostname returned by gethostbyaddr() is checked, then
    possibly existing aliases. In case no FQDN is available, hostname
    is returned.
    """
    name = string.strip(name)
    if not name or name == '0.0.0.0':
        name = socket.gethostname()
    try:
        hostname, aliases, ipaddrs = socket.gethostbyaddr(name)
    except socket.error:
        pass
    else:
        aliases.insert(0, hostname)
        for name in aliases:
            if '.' in name:
                break
        else:
            name = hostname
    return name

def get_user_id(email='', realname=''):
    uid = os.getuid()
    info = pwd.getpwuid(uid)
    email = (os.environ.get('REPORTBUGEMAIL', email) or
             os.environ.get('DEBEMAIL') or os.environ.get('EMAIL'))
    
    email = email or find_rewritten(info[0]) or info[0]

    if '@' not in email:
        if os.path.exists('/etc/mailname'):
            domainname = string.strip(open('/etc/mailname').readline())
        else:
            domainname = getfqdn()

        email = email+'@'+domainname

    # Handle EMAIL if it's formatted as 'Bob <bob@host>'.
    if '<' in email:
        # Handle unquoted realname parts
        m = re.match(r'([^"].+[^"])*\s+<(.*)>', email)
        if not m: return email
        realname, email = m.groups()
        
    realname = realname or string.split(info[4], ',')[0]
    realname = (os.environ.get('DEBFULLNAME') or os.environ.get('DEBNAME') or
                os.environ.get('NAME') 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: ')
    priorityre = re.compile('Priority: ')
    dependsre = re.compile('(Pre-)?Depends: ')
    conffilesre = re.compile('Conffiles: ')
    maintre = re.compile('Maintainer: ')
    statusre = re.compile('Status: ')
    originre = re.compile('Origin: ')
    bugsre = re.compile('Bugs: ')
    descre = re.compile('Description: ')
    fullre = re.compile(' ')
    srcre = re.compile('Source: ')

    pkgversion = pkgavail = depends = maintainer = status = origin = None
    bugs = vendor = priority = desc = src_name = None
    conffiles = []
    fulldesc = []
    confmode = 0
    
    try:
        x = os.getcwd()
    except OSError:
        os.chdir('/')
    pipe = os.popen("COLUMNS=79 dpkg --status '%s' 2>/dev/null" % package)
    
    for line in pipe.readlines():
        line = string.rstrip(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, ": ", 1)
        elif statusre.match(line):
            (crud, status) = string.split(line, ": ", 1)
        elif priorityre.match(line):
            (crud, priority) = string.split(line, ": ", 1)
        elif packagere.match(line):
            (crud, pkgavail) = string.split(line, ": ", 1)
        elif originre.match(line):
            (crud, origin) = string.split(line, ": ", 1)
        elif bugsre.match(line):
            (crud, bugs) = string.split(line, ": ", 1)
        elif descre.match(line):
            (crud, desc) = string.split(line, ": ", 1)
        elif dependsre.match(line):
            (crud, thisdepends) = string.split(line, ": ", 1)
            if depends:
                depends = depends+', '+thisdepends
            else:
                depends = thisdepends
        elif conffilesre.match(line):
            confmode = 1
        elif maintre.match(line):
            crud, maintainer = string.split(line, ": ", 1)
        elif srcre.match(line):
            crud, src_name = string.split(line, ": ", 1)
        elif desc and line[0]==' ':
            fulldesc.append(line)

    pipe.close()

    installed = 0
    if status:
        state = string.split(status)[2]
        installed = (state not in ('config-files', 'not-installed'))

    reportinfo = None
    if bugs:
        reportinfo = debianbts.parse_bts_url(bugs)
    elif origin:
        if debianbts.SYSTEMS.has_key(origin):
            vendor = debianbts.SYSTEMS[origin]['name']
            reportinfo = (debianbts.SYSTEMS[origin]['type'],
                          debianbts.SYSTEMS[origin]['btsroot'])
            
        else:
            vendor = string.capitalize(origin)
    else:
        vendor = ''

    return (pkgversion, pkgavail, depends, conffiles, maintainer, installed,
            origin, vendor, reportinfo, priority, desc, src_name,
            string.join(fulldesc, os.linesep))

dbase = None
def get_dpkg_database():
    global dbase

    if not dbase:
        dbase = open('/var/lib/dpkg/status').read()

    return dbase

avail = None
def get_avail_database():
    global avail

    if not avail:
        avail = open('/var/lib/dpkg/available').read()

    return avail

def get_source_package(package):
    """Return any binary packages provided by a source package."""
    dbase = get_avail_database()
    packages = []
    packob = re.compile(r'^Package: (?P<pkg>.*)$', re.MULTILINE)
    descob = re.compile(r'^Description: (?P<desc>.*)$', re.MULTILINE)
    searchob = re.compile(r'^(Package|Source): '+re.escape(package)+r'$',
                          re.MULTILINE)
    
    packinfo = string.split(dbase, '\n\n')
    for p in packinfo:
        match = searchob.search(p)
        if match:
            packname = packdesc = ''
            namematch, descmatch = packob.search(p), descob.search(p)

            if namematch:
                packname = namematch.group('pkg')
            if descmatch:
                packdesc = descmatch.group('desc')
            
            if packname:
                packages.append( (packname, packdesc) )

    packages.sort()
    return packages

def get_package_info(packages):
    dbase = get_dpkg_database()
    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 packages_providing(package):
    aret = get_package_info([('', package)])
    ret = []
    for pkg in aret:
        ret.append( (pkg[0], pkg[3]) )
        if not pkg[2]: return []
    
    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 %-.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)

    try:
        x = os.getcwd()
    except OSError:
        os.chdir('/')
    pipe = os.popen("COLUMNS=79 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, justification,
                          depinfo, confinfo, foundfile='', incfiles='',
                          system='debian', exinfo=0, type=None, klass='',
                          subject='', tags='', body=''):
    if debianbts.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('COLUMNS=79 dpkg '
                                     '--print-installation-architecture')
        debinfo = "Debian Release: %s\nArchitecture: %s\n" % (debvers, debarch)
    else:
        debinfo = ''

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

    locinfo = []
    langsetting = os.environ.get('LANG', 'C')
    allsetting = os.environ.get('LC_ALL', '')
    for setting in ('LANG', 'LC_CTYPE'):
        if setting == 'LANG':
            env = langsetting
        else:
            env = allsetting or os.environ.get(setting, langsetting)
        locinfo.append('%s=%s' % (setting, env))
    locinfo = string.join(locinfo, ', ')

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

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

    if justification:
        severity = severity + 'Justification: %s\n' % justification

    if tags:
        fileinfo = fileinfo + 'Tags: %s\n' % tags

    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 = "\n"
    if not exinfo:
        if type == 'gnats':
            report = ">Synopsis: %s\n>Confidential: no\n" % subject
            if package == 'debian-general':
                report = report + ">Category: %s\n" % package
            else:
                report = report + ">Category: debian-packages\n"\
                         ">Release: %s_%s\n" % (package, pkgversion)

            if severity:
                report = report + ">"+severity

            if klass:
                report = report + ">Class: %s\n" % klass
            report = report + (
                ">Description:\n\n"
                "  <describe the bug here; use as many lines as you need>\n\n"
                ">How-To-Repeat:\n\n"
                "  <show how the bug is triggered>\n\n"
                ">Fix:\n\n"
                "  <if you have a patch or solution, put it here>\n\n"
                ">Environment:\n")
        else:
            report = "Package: %s\nVersion: %s\n%s%s\n" % (package, pkgversion,
                                                           severity, fileinfo)
    else:
        report = "Followup-For: Bug #%d\nPackage: %s\nVersion: %s\n" % (
            exinfo, package, pkgversion)

    report = report + body + """%s

%s-- System Information
%sKernel: %s
Locale: %s
%s%s""" % (incfiles, specials, debinfo, string.join(os.uname()), locinfo,
           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')

CONFIG_ARGS = ('sendto', 'severity', 'mua', 'mta', 'email', 'realname', 'bts',
               'replyto', 'http_proxy', 'smtphost', 'editor', 'mua', 'mta',
               'justification', 'sign', 'nocc', 'nocompress', 'dontquery',
               'noconf', 'use_ldap', 'mirrors', 'headers', 'interface',
               'template')

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 debianbts.SEVERITIES.keys():
                        args['severity'] = 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 -use -file'
                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-query-bts':
                    args['dontquery'] = 1
                elif token == 'query-bts':
                    args['dontquery'] = 0
                elif token == 'config-files':
                    args['noconf'] = 0
                elif token == 'no-config-files':
                    args['noconf'] = 1
                elif token == 'ldap':
                    args['use_ldap'] = 1
                elif token == 'no-ldap':
                    args['use_ldap'] = 0
                elif token in ('printonly', 'template'):
                    args[token] = 1
                elif token in ('email', 'realname', 'replyto', 'http_proxy',
                               'smtphost', 'editor', 'mua', 'mta',
                               'justification'):
                    args[token] = 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 == 'ui':
                    token = string.lower(lex.get_token())
                    if token in VALID_UIS:
                        args['interface'] = token
                elif token == 'bts':
                    token = string.lower(lex.get_token())
                    if token in debianbts.SYSTEMS.keys():
                        args['bts'] = token
                elif token == 'mirror':
                    args['mirrors'] = args.get('mirrors', []) + \
                                      [lex.get_token()]
                elif token in ('reportbug_version', 'mode'):
                    token = lex.get_token()
                else:
                    sys.stderr.write('Unrecognized token: '+token+'\n')

                token = lex.get_token()

    return args

def parse_bug_control_file(filename):
    submitas = submitto = None
    fh = open(filename)
    for line in fh.readlines():
        line = string.strip(line)
        parts = string.split(line, ': ')
        if len(parts) != 2:
            continue

        header, data = parts
        if string.lower(header) == 'submit-as':
            submitas = data
        elif string.lower(header) == 'send-to':
            submitto = data

    return submitas, submitto
