#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Provides an apt backend to PackageKit

Copyright (C) 2007 Ali Sabil <ali.sabil@gmail.com>
Copyright (C) 2007 Tom Parker <palfrey@tevp.net>
Copyright (C) 2008-2009 Sebastian Heinlein <glatzor@ubuntu.com>
Copyright (C) 2012 Martin Pitt <martin.pitt@ubuntu.com>

Licensed under the GNU General Public License Version 2

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.
"""

__author__  = "Sebastian Heinlein <devel@glatzor.de>"

import da
tetime
import errno
import fcntl
import gdbm
import glob
import gzip
import locale
import logging
import logging.handlers
import os
import pty
import re
import signal
import socket
import stat
import string
import subprocess
import sys
import time
import fnmatch

import apt
import apt.debfile
import apt_pkg

try:
    import pkg_resources
except ImportError:
    # no plugin support available
    pkg_resources = None

from packagekit.backend import (PackageKitBaseBackend, format_string)
from packagekit import enums

logging.basicConfig(format="%(levelname)s:%(message)s")
pklog = logging.getLogger("PackageKitBackend")
pklog.setLevel(logging.NOTSET)

try:
    _syslog = logging.handlers.SysLogHandler("/dev/log",
                                      logging.handlers.SysLogHandler.LOG_DAEMON)
    formatter = logging.Formatter('PackageKit: %(levelname)s: %(message)s')
    _syslog.setFormatter(formatter)
    pklog.addHandler(_syslog)
except:
    pass

# Xapian database is optionally used to speed up package description search
XAPIAN_DB_PATH = os.environ.get("AXI_DB_PATH", "/var/lib/apt-xapian-index")
XAPIAN_DB = XAPIAN_DB_PATH + "/index"
XAPIAN_DB_VALUES = XAPIAN_DB_PATH + "/values"
XAPIAN_SUPPORT = False
try:
    import xapian
except ImportError:
    pass
else:
    if os.access(XAPIAN_DB, os.R_OK):
        pklog.debug("Use XAPIAN for the search")
        XAPIAN_SUPPORT = True

# SoftwareProperties is required to proivde information about repositories
try:
    import softwareproperties.SoftwareProperties
except ImportError:
    REPOS_SUPPORT = False
else:
    REPOS_SUPPORT = True

# Check if update-manager-core is installed to get aware of the
# latest distro releases
try:
    from UpdateManager.Core.MetaRelease import MetaReleaseCore
except ImportError:
    META_RELEASE_SUPPORT = False
else:
    META_RELEASE_SUPPORT = True


# Set a timeout for the changelog download
socket.setdefaulttimeout(2)

# Required for daemon mode
os.putenv("PATH",
          "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")

# Map Debian sections to the PackageKit group name space
SECTION_GROUP_MAP = {
    "admin" :enums.GROUP_ADMIN_TOOLS,
    "base" :enums.GROUP_SYSTEM,
    "comm" :enums.GROUP_COMMUNICATION,
    "devel" :enums.GROUP_PROGRAMMING,
    "doc" :enums.GROUP_DOCUMENTATION,
    "editors" :enums.GROUP_PUBLISHING,
    "electronics" :enums.GROUP_ELECTRONICS,
    "embedded" :enums.GROUP_SYSTEM,
    "games" :enums.GROUP_GAMES,
    "gnome" :enums.GROUP_DESKTOP_GNOME,
    "graphics" :enums.GROUP_GRAPHICS,
    "hamradio" :enums.GROUP_COMMUNICATION,
    "interpreters" :enums.GROUP_PROGRAMMING,
    "kde" :enums.GROUP_DESKTOP_KDE,
    "libdevel" :enums.GROUP_PROGRAMMING,
    "libs" :enums.GROUP_SYSTEM,
    "mail" :enums.GROUP_INTERNET,
    "math" :enums.GROUP_SCIENCE,
    "misc" :enums.GROUP_OTHER,
    "net" :enums.GROUP_NETWORK,
    "news" :enums.GROUP_INTERNET,
    "oldlibs" :enums.GROUP_LEGACY,
    "otherosfs" :enums.GROUP_SYSTEM,
    "perl" :enums.GROUP_PROGRAMMING,
    "python" :enums.GROUP_PROGRAMMING,
    "science" :enums.GROUP_SCIENCE,
    "shells" :enums.GROUP_SYSTEM,
    "sound" :enums.GROUP_MULTIMEDIA,
    "tex" :enums.GROUP_PUBLISHING,
    "text" :enums.GROUP_PUBLISHING,
    "utils" :enums.GROUP_ACCESSORIES,
    "web" :enums.GROUP_INTERNET,
    "x11" :enums.GROUP_DESKTOP_OTHER,
    "unknown" :enums.GROUP_UNKNOWN,
    "alien" :enums.GROUP_UNKNOWN,
    "translations" :enums.GROUP_LOCALIZATION,
    "metapackages" :enums.GROUP_COLLECTIONS,
    }

# Regular expressions to detect bug numbers in changelogs according to the
# Debian Policy Chapter 4.4. For details see the footnote 16:
# http://www.debian.org/doc/debian-policy/footnotes.html#f16
MATCH_BUG_CLOSES_DEBIAN=r"closes:\s*(?:bug)?\#?\s?\d+(?:,\s*(?:bug)?\#?\s?\d+)*"
MATCH_BUG_NUMBERS=r"\#?\s?(\d+)"
# URL pointing to a bug in the Debian bug tracker
HREF_BUG_DEBIAN="http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=%s"

MATCH_BUG_CLOSES_UBUNTU = r"lp:\s+\#\d+(?:,\s*\#\d+)*"
HREF_BUG_UBUNTU = "https://bugs.launchpad.net/bugs/%s"

# Regular expression to find cve references
MATCH_CVE="CVE-\d{4}-\d{4}"
HREF_CVE="http://web.nvd.nist.gov/view/vuln/detail?vulnId=%s"

SYNAPTIC_PIN_FILE = "/var/lib/synaptic/preferences"

# After the given amount of seconds without any updates on the console or
# progress kill the installation
TIMEOUT_IDLE_INSTALLATION = 10 * 60 * 10000

# Required to get translated descriptions
try:
    locale.setlocale(locale.LC_ALL, "")
except locale.Error:
    pklog.debug("Failed to unset LC_ALL")

# Required to parse RFC822 time stamps
try:
    locale.setlocale(locale.LC_TIME, "C")
except locale.Error:
    pklog.debug("Failed to unset LC_TIME")

def catch_pkerror(func):
    """Decorator to catch a backend error and report
    it correctly to the daemon.
    """
    def _catch_error(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except PKError as error:
            backend = args[0]
            backend.error(error.enum, error.msg, error.exit)
    return _catch_error

def lock_cache(func):
    """Lock the system package cache before excuting the decorated function and
    release the lock afterwards.
    """
    def _locked_cache(*args, **kwargs):
        backend = args[0]
        backend.status(enums.STATUS_WAITING_FOR_LOCK)
        while True:
            try:
                # see if the lock for the download dir can be acquired
                # (work around bug in python-apt/apps that call _fetchArchives)
                lockfile = apt_pkg.config.find_dir("Dir::Cache::Archives") + \
                           "lock"
                lock = apt_pkg.get_lock(lockfile)
                if lock < 0:
                    raise SystemError("failed to lock '%s'" % lockfile)
                else:
                    os.close(lock)
                # then lock the main package system
                apt_pkg.pkgsystem_lock()
            except SystemError:
                time.sleep(3)
            else:
                break
        try:
            func(*args, **kwargs)
        finally:
            backend._unlock_cache()
    return _locked_cache


class PKError(Exception):

    def __init__(self, enum, msg, exit=True):
        self.enum = enum
        self.msg = msg
        self.exit = exit

    def __str__(self):
        return "%s: %s" % (self.enum, self.msg)


class PackageKitOpProgress(apt.progress.base.OpProgress):

    """Handle the cache opening progress."""

    def __init__(self, backend, start=0, end=100, progress=True):
        self._backend = backend
        apt.progress.base.OpProgress.__init__(self)
        self.steps = []
        for val in [0.12, 0.25, 0.50, 0.75, 1.00]:
            step = start + (end - start) * val
            self.steps.append(step)
        self.pstart = float(start)
        self.pend = self.steps.pop(0)
        self.pprev = None
        self.show_progress = progress

    # OpProgress callbacks
    def update(self, percent=None):
        if percent is None:
            return
        progress = int(self.pstart + percent / 100 * (self.pend - self.pstart))
        if self.show_progress == True and self.pprev < progress:
            self._backend.percentage(progress)
            self.pprev = progress

    def done(self):
        self.pstart = self.pend
        try:
            self.pend = self.steps.pop(0)
        except:
            pklog.warning("An additional step to open the cache is required")


class PackageKitAcquireProgress(apt.progress.base.Acqui