#!/usr/bin/ruby
#
# apt-listbugs: retrieves bug reports and lists them
#
# Copyright (C) 2002       Masato Taruishi <taru@debian.org>
# Copyright (C) 2006-2008  Junichi Uekawa <dancer@debian.org>
# Copyright (C) 2007       Famelis George <famelis@otenet.gr>
# Copyright (C) 2008-2014  Francesco Poli <invernomuto@paranoici.org>
# Copyright (C) 2009       Ryan Niebur <ryan@debian.org>
# Copyright (C) 2012       Justin B Rye <jbr@edlug.org.uk>
# Copyright (C) 2013       Google Inc
#
#  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 General Public License for more details.
#
#  You should have received a copy of the GNU General Public License with
#  the Debian GNU/Linux distribution in file /usr/share/common-licenses/GPL-2;
#  if not, write to the Free Software Foundation, Inc., 59 Temple Place,
#  Suite 330, Boston, MA  02111-1307  USA
#
#
=begin

== NAME

apt-listbugs - Lists critical bugs before each APT installation/upgrade

== SYNOPSIS

apt-listbugs [options] <command> [arguments]

== DESCRIPTION

apt-listbugs is a tool which retrieves bug reports from the Debian
Bug Tracking System and lists them. In particular, it is intended to
be invoked before each installation or upgrade by APT, or other
similar package managers, in order to check whether the
installation/upgrade is safe.

== USAGE

apt-listbugs [-h] [-v] [-s <severities>] [-T <tags>] [-S <states>] [-B <bug#>] [-D] [-H <hostname>] [-p <port>] [-P <priority>] [-E <title>] [-q] [-C <apt.conf>] [-F] [-y] [-n] [-d] <command> [arguments]

== OPTIONS

* -h, --help

  Print usage help and exit.

* -v, --version

  Print version number and exit.

* -s <severities>, --severity <severities>

  Filter (and sort) bugs by severity, showing only the bugs matching
  specified values. List the bug severities that you want to see,
  separated by commas and in the desired order. Possible values are
  "critical", "grave", "serious", "important", "normal", "minor",
  "wishlist", or the special value "all" to disable filtering.
  Default: [critical,grave,serious]. The default list may be
  changed by setting the AptListbugs::Severities configuration option.

* -T <tags>, --tag <tags>

  Filter bugs by tags, showing only the bugs matching _all_ specified
  values. List the tags that you want to see, separated by commas.
  Default: no filter. Possible values include "confirmed,l10n" to show
  only bugs that have both these tags.

* -S <states>, --stats <states>

  Filter (and sort) bugs by pending-state, showing only the bugs
  matching specified values. List the pending-state categories that
  you want to see, separated by commas and in the desired order.
  Default: [pending,forwarded,pending-fixed,fixed,done]. Possible values are:

    pending = open bug
    forwarded = marked as "forwarded"
    pending-fixed = tagged as "pending"
    fixed = tagged as "fixed"
    absent = not found in this distribution/architecture
    done = resolved in some version for this
           distribution/architecture

  Note that a bug can only match one such state (when multiple
  conditions on this list match, the later one takes priority), and
  that "pending" does not mean "tagged as pending".

* -B <bug#>, --bugs <bug#>

  Filter bugs by number, showing only the bugs directly specified.
  List the bug numbers that you want to see, separated by commas
  (e.g. "123456,567890,135792"). Default: no filter.

* -D, --show-downgrade

  Show bugs of downgraded packages. (apt mode only)

* -H <hostname>, --hostname <hostname>

  Specifies the hostname of the Debian Bug Tracking System [bugs.debian.org].

* -p <port>, --port <port>

  Specifies the port number of the web interface of the Debian Bug
  Tracking System [80].

* -P <priority>, --pin-priority <priority>

  Specifies Pin-Priority value [1000].

* -E <title>, --title <title>

  Specifies the title of RSS output.

* -q, --quiet

  Don't display progress bar. This option is assumed if stdout is not a
  terminal.

* -C <apt.conf>, --aptconf <apt.conf>

  Specifies the APT configuration file to use.

* -F, --force-pin

  When in apt mode, assumes that you want to automatically pin all buggy
  packages without any prompt.  This option is assumed if stdout is not a
  terminal.

* -y, --force-yes

  Assumes that you select yes for all questions.  When in apt mode,
  this implies that you accept to continue with the installation/upgrade,
  even when bugs are found or errors occur.

* -n, --force-no

  Assumes that you select no for all questions.  When in apt mode,
  this implies that you want to abort the installation/upgrade, as
  soon as bugs are found or errors occur.  This option is assumed
  if stdout is not a terminal.

* -d, --debug

  Give extra debug output, important for debugging problems. Please
  include -d when reporting problems.

== COMMANDS

: apt

  Reads package actions from a file descriptor specified in the
  APT_HOOK_INFO_FD environment variable (typically provided by APT
  or other compatible package manager; Pre-Install-Pkgs hook info
  protocol version 3 is expected - see apt.conf(5) for more details).

: list [<package1[:arch][/version]> <package2[:arch][/version]>...]

  Reads package names from the arguments and simply lists bugs of
  these packages. Package versions may be specified with a slash, as in
  apt/1.0 for example. Package architectures may be specified with a colon,
  as in apt:amd64 or apt:amd64/1.0 (but please note that the Debian Bug
  Tracking System does not distinguish the architectures, hence the
  same bugs will be listed, regardless of the specified architecture).

: rss [<package1[:arch][/version]> <package2[:arch][/version]>...]

  Reads package names from the arguments and lists bugs of these packages
  in RSS format. Again, package versions may be specified with a slash
  and architectures with a colon.

== ENVIRONMENT VARIABLES

: APT_LISTBUGS_FRONTEND

  If this variable is set to "none", apt-listbugs will not execute at all;
  this might be useful if you would like to script the use of a program that
  calls apt-listbugs.

: http_proxy

  If http_proxy is set, the value is used for HTTP Proxy, unless
  proxy settings are found in APT configuration (see below).

: APT_HOOK_INFO_FD

  File descriptor from which package actions will be read (APT or other
  compatible package managers are expected to write information to this
  file descriptor and to properly set this environment variable).

== CONFIGURATION FILE

apt-listbugs understands APT configuration file (see apt.conf). The
notable configuration options are

: Acquire::HTTP::Proxy

  Default HTTP Proxy setting (overrides any http_proxy environment variable
  value).
  An empty string or the special keyword 'DIRECT' will disable proxy.

: Acquire::HTTP::Proxy::bugs.debian.org

  Specific HTTP Proxy setting (overrides the default HTTP Proxy setting).
  Useful for setting HTTP proxy for apt-listbugs.
  The special keyword 'DIRECT' will disable proxy.

: AptListbugs::Severities

  Default (comma-separated) list of bug severities to be shown. When
  this option is not set, the list is [critical,grave,serious], unless
  explicitly altered by using the "-s" command-line option. On the other
  hand, when this option is set, the list of severities is its value,
  unless explicitly altered by using the "-s" command-line option.

: AptListbugs::IgnoreRegexp

  Bugs to ignore when in apt mode. This is evaluated using Ruby regular
  expressions: if the bug title matches, the bug is ignored. Default:
  nothing. A possible suggested value is "FTBFS", since those bugs tend
  to not affect the user.

: AptListbugs::ParseStep

  Maximum number of bug reports to be queried (on the Debian Bug Tracking
  System) and parsed in a single batch. Default value is 200. The query
  and parse operation is performed in batches of at most ParseStep bugs,
  for performance reasons; setting a lower value may slow down apt-listbugs,
  but may increase reliability on poor network links.

== OUTPUT EXAMPLE

  [bug severity] bugs of [package] ([current version] -> [package version to be installed]) <[state of bug report]>
   [bug #] - [bug title] [(Fixed: fixed version, if it's fixed in a future version)]

  e.g.


  Retrieving bug reports... Done
  Parsing Found/Fixed information... Done
  important bugs of apt-listbugs (0.0.47 -> 0.0.49) <Outstanding>
   #332442 - apt-listbugs: Apt-listbugs doesn't actually download any bug reports
   #389903 - apt-listbugs: Does not offer to exit if timeout occurs fetching reports
  Summary:
   apt-listbugs(2 bugs)



== EXIT STATUS

: 0

  If the program ran successfully and (when in apt mode) you decided
  to continue with the installation/upgrade. Or otherwise, if a SIGUSR1
  was received (for instance because you issued the command "killall
  -USR1 apt-listbugs").

: 1

  If an error occurred.

: 10

  If the program ran successfully in apt mode, but you decided to abort
  the installation/upgrade.

: 130

  If a SIGINT was received (for instance because you pressed [Ctrl+C]).

N.B.: When the program is invoked by APT, any non-zero exit status will
cause the installation/upgrade to be aborted.

== AUTHORS

apt-listbugs was originally written by Masato Taruishi
<taru@debian.org>, and rewritten by Junichi Uekawa
<dancer@debian.org> in 2006 to handle BTS Versioning features and the
SOAP interface. The --bugs option was added by Francesco Poli
<invernomuto@paranoici.org> in 2008. During 2009-2010, apt-listbugs was
maintained by Francesco Poli and Ryan Niebur <ryan@debian.org>, then, during
2011-2012, by Francesco Poli and Thomas Mueller <thomas.mueller@tmit.eu>.
It is currently maintained by Francesco Poli.

The latest source code is available from
http://anonscm.debian.org/gitweb/?p=apt-listbugs/apt-listbugs.git

== SEE ALSO

apt.conf(5), sensible-browser(1), www-browser(1), querybts(1)

=end


# exit gracefully when user presses [Ctrl+C]
Signal.trap("SIGINT") { $stderr.puts "Interrupted"; exit 130 }

# exit successfully when SIGUSR1 is received
Signal.trap("SIGUSR1") { $stderr.puts "Emergency exit"; exit! 0 }


if File.expand_path(__FILE__).match(/^\/usr\/s?bin\//)
  $LOAD_PATH.unshift("/usr/share/apt-listbugs")
  $VERSION = `dpkg-query -W -f='${Version}' apt-listbugs`
else
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "lib"))
  $VERSION = `dpkg-parsechangelog -SVersion`
end

require 'getoptlong'
require 'gettext'
include GetText

GetText::bindtextdomain("apt-listbugs")

begin
  require 'debian'
  require 'unicode'
rescue LoadError
  # TRANSLATORS: "E: " is a label for error messages; you may translate it with a suitable abbreviation of the word "error"
  $stderr.puts _("E: ") + "#{$!}"
  $stderr.puts _("This may be caused by a package lacking support for the ruby interpreter in use. Try to fix the situation with the following commands:")
  $stderr.puts "  # mv /etc/apt/apt.conf.d/10apt-listbugs /root/"
  $stderr.puts "  # aptitude update"
  $stderr.puts "  # aptitude install ruby-debian ruby-unicode ruby"
  $stderr.puts "  # mv /root/10apt-listbugs /etc/apt/apt.conf.d/"
  exit 1
end
require 'debian/bug'
require 'debian/bts'
require 'thread'
require 'tempfile'
require 'rss/maker'
require 'apt-listbugs/logic'

## main from here

# Drop out as early as possible if this env var is set.
if ENV["APT_LISTBUGS_FRONTEND"] == "none"
  exit 0
end
# handle options
config = AppConfig.new
config.parse_options
Factory.config = config

# handle arguments
new_pkgs = {}
cur_pkgs = {}
native_arch = nil
case config.command
when "apt"
  # parse APT VERSION 3 input.
  state=1
  apt_hook_fd = ENV["APT_HOOK_INFO_FD"]
  puts if $DEBUG
  puts "Preparing to read info from file descriptor \"#{apt_hook_fd}\"" if $DEBUG
  if apt_hook_fd.nil?
    $stderr.print _("E: ") + _("APT_HOOK_INFO_FD is undefined.\n")
    exit 1
  end
  apt_hook_fd = apt_hook_fd.to_i
  if apt_hook_fd == 0
    $stderr.print _("E: ") + _("APT_HOOK_INFO_FD is not correctly defined.\n")
    exit 1
  end
  begin
    apt_hook_stream = IO.open(apt_hook_fd, 'r')
  rescue Errno::ENOENT, Errno::EBADF
    $stderr.puts _("E: ") + sprintf(_("Cannot read from file descriptor %d"), apt_hook_fd)
    exit 1
  end
  puts if $DEBUG
  puts "Pre-Install-Pkgs hook info:" if $DEBUG
  apt_hook_stream.each { |pkg|
    pkg=pkg.rstrip
    case state
    when 1
      # the version header, only one line.
      if pkg == "VERSION 3"
        puts "#{pkg}" if $DEBUG
        state=2
      else
        $stderr.print _("E: ") + _("APT Pre-Install-Pkgs is not giving me expected 'VERSION 3' string.\n")
        exit 1
      end
    when 2
      # APT configuration lines
      case pkg
      when ""
        puts "#{pkg}" if $DEBUG
        state=3
      when /^APT::Architecture=(.*)/
        if $1
          puts "#{pkg}" if $DEBUG
          native_arch=$1
        end
      when /^quiet=(.*)/
        if $1.to_i > 0
          puts "#{pkg}" if $DEBUG
          config.quiet=true
        end
      end
    when 3
      # package action lines
      puts "#{pkg}" if $DEBUG
      pkg_name, old_ver, old_arch, old_ma, direction, new_ver, new_arch, new_ma, filename = pkg.split(" ")
      case filename
      when "**CONFIGURE**"
        # none
      when "**REMOVE**"
        # none
      when nil
        $stderr.print _("E: ") + _("APT Pre-Install-Pkgs is giving me fewer fields than expected.\n")
        exit 1
      else
        case direction
        when "="
          # no version change, hence no new bug can be introduced
          # into the system by this package
        when ">", "<"
          # ">" means downgrade, "<" means upgrade
          if ( config.show_downgrade or direction == "<" )
            if ( pkg_name != nil and new_ver != "-" )
              f = {}
              f["package"] = pkg_name
              f["version"] = new_ver
              # pkg_key is the package full name (<pkg_name> for native and
              # "all" architecture packages, <pkg_name>:<arch> for foreign
              # architecture packages)
              pkg_key = pkg_name
              if ( new_arch != nil and new_arch != "all" and new_arch != native_arch )
                pkg_key = pkg_name + ":" + new_arch
              end
              new_pkgs[pkg_key] = f
              if ( old_ver != "-" )
                f = {}
                f["package"] = pkg_name
                f["version"] = old_ver
                cur_pkgs[pkg_key] = f
              end
            end
          end
        else
          $stderr.print _("E: ") + _("APT Pre-Install-Pkgs is giving me an invalid direction of version change.\n")
          exit 1
        end
      end
    end
  }
  apt_hook_stream.close
  puts if $DEBUG
when "list", "rss"
  ARGV.each { |pkg_key|
    # parse 'apt-listbugs list pkg_name:arch/version ... ' combination
    if ( pkg_key != nil )
      f = {}
      if /^(.*)\/(.*)$/ =~ pkg_key
        pkg_key = $1
        f["version"] = $2
      end
      if /^(.*):(.*)$/ =~ pkg_key
        f["package"] = $1
      else
        f["package"] = pkg_key
      end
      new_pkgs[pkg_key] = f
    end
  }
end

exit 0 if new_pkgs.size == 0

Factory::BugsFactory.delete_ignore_pkgs(new_pkgs) if config.command == "apt"

exit 0 if new_pkgs.size == 0

# build the multiarch map: for each pkg_name, list the corresponding pkg_keys
ma_copies = {}
new_pkgs.each_pair { |pkg_key, pkg|
  pkg_name = pkg["package"]
  if ( pkg_name != nil )
    ma_copies[pkg_name] = [] if ma_copies[pkg_name] == nil
    ma_copies[pkg_name] << pkg_key
  end
}

# read bug reports
begin
  bugs = Factory::BugsFactory.create(ma_copies) { |msg, val|
    config.frontend.progress(msg, val) if config.quiet == false
  }
rescue
  config.frontend.puts _("E: ") + "#{$!}"
  exit 1
end

Factory::BugsFactory.delete_ignore_bugs(bugs) if config.command == "apt"
Factory::BugsFactory.delete_regexp_bugs(bugs, config.ignore_regexp) if config.command == "apt" and config.ignore_regexp
Factory::BugsFactory.delete_uninteresting_bugs(bugs) if config.fbugs
Factory::BugsFactory.delete_unwanted_tag_bugs(bugs) if config.tag
begin
  Factory::BugsFactory.delete_irrelevant_bugs(bugs, cur_pkgs, new_pkgs) { |msg, val|
    config.frontend.progress(msg, val) if config.quiet == false
  }
rescue
  config.frontend.puts _("E: ") + "#{$!}"
  exit 1
end

exit 0 if config.command != "rss" && bugs.size == 0

# read done. now starting viewer
viewer = nil
case config.command
when "apt"
  viewer = Viewer::SimpleViewer.new(config)
when "list"
  viewer = Viewer::SimpleViewer.new(config)
when "rss"
  viewer = Viewer::RSSViewer.new(config)
end
if viewer.view(new_pkgs, cur_pkgs, bugs) == false
  ErrorWarning =  _("****** Exiting with an error in order to stop the installation. ******")
  ErrorWarningHeader = "*" * Unicode.width(ErrorWarning)
  config.frontend.puts ErrorWarningHeader
  config.frontend.puts ErrorWarning
  config.frontend.puts ErrorWarningHeader
  config.frontend.close
  exit 10
end
config.frontend.close
