#!/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-2020  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.,
#  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
#
=begin

= NAME

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

= SYNOPSIS

(({apt-listbugs})) [(({-s})) ((*severities*))] [(({-T})) ((*tags*))]
[(({-S})) ((*states*))] [(({-B})) ((*bug#*))] [(({-D}))]
[(({-H})) ((*hostname*))] [(({-p})) ((*port*))] [(({-P})) ((*priority*))]
[(({-E})) ((*title*))] [(({-q}))] [(({-C})) ((*apt.conf*))] [(({-F}))]
[(({-N}))] [(({-y}))] [(({-n}))] [(({-d}))]
((*command*)) [((*argument*)) ...]

(({apt-listbugs -h}))

(({apt-listbugs -v}))

= 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
compatible package managers, in order to check whether the
installation/upgrade is safe.

In the typical use case, the user is installing or upgrading a number
of packages with APT or some other compatible package manager. Before the
package installation or upgrade is actually performed, (({apt-listbugs}))
is automatically invoked: it queries the Debian Bug Tracking System for
bugs (of certain configured severities) that would be introduced into
the system by the installation or upgrade; if any such bug is found,
(({apt-listbugs})) warns the user and asks how to proceed. Among other
things, the user has the opportunity to continue, to abort the installation
or upgrade, or to pin some packages (so that the unsafe installation or
upgrade is deferred). However, pinning is not effective immediately, and
requires restarting the APT session (by aborting and then re-running
the same APT command).

Each package pin is automatically removed by a daily cron job (or by an
equivalent systemd timer), as soon as the corresponding bug is fixed in
(or no longer affects) the package version available for installation
or upgrade. When the pin is removed, the installation or upgrade of the
package becomes possible again.

In order for the automatic pin removal to work correctly, the daily cron
job or systemd timer has to be actually executed: if systemd is used
as init system, this should be taken care of automatically; otherwise,
if the system is up and running almost 24/7, then (({cron})) should
suffice; in all other cases, the installation of (({anacron})) is
recommended. Moreover the Internet link must be working, while the
daily job is run. Finally, the APT package lists should be kept up-to-date.

= OPTIONS

: -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. Default:
  (({bugs.debian.org})) .

: -p ((*port*)), --port ((*port*))

  Specifies the port number of the web interface of the Debian Bug
  Tracking System. Default: 80.

: -P ((*priority*)), --pin-priority ((*priority*))

  Specifies (({Pin-Priority})) value. Default: 30000.

: -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 an additional APT configuration file to use.  This file will
  be read after the default APT configuration files.

: -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, unless the (({-N})) command-line option is used.

: -N, --force-no-pin

  When in apt mode, never automatically pin any package without prompt.
  This is the default behavior, as long as stdout is 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, unless the (({-y})) command-line option is used.

: -a, --force-default

  Assumes that you select the default (automatic) reply for all questions.
  When in apt mode, this implies that you accept to continue with the
  installation/upgrade, even when bugs are found, but not when errors occur.

: -d, --debug

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

: -h, --help

  Print usage help and exit.

: -v, --version

  Print version number and exit.

= 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 [((*package*))[:((*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 [((*package*))[:((*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})) reads the APT configuration (see (({apt.conf}))(5)
for more details). The following configuration options are recognized:

: 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-Auto-Detect

  Automatic HTTP Proxy discovery (overrides the default HTTP Proxy setting
  and any $(({http_proxy})) environment variable value).
  It can be used to specify an external command that is expected to output
  the proxy on stdout.

: Acquire::http::Proxy::bugs.debian.org

  Specific HTTP Proxy setting (overrides any other 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::QueryStep

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

: 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]>
   [bN] - [#bug] - [bug title] [(Fixed: fixed version, if it's fixed in a future version)]
  Summary:
   [package]([number of] bugs)

e.g.:

  important bugs of apt-listbugs (0.0.47 -> 0.0.49) <Outstanding>
   b1 - #332442 - apt-listbugs: Apt-listbugs doesn't actually download any bug reports
   b2 - #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.

= FILES

: ((*/etc/apt/preferences.d/apt-listbugs*))

  Version preferences file fragment for APT managed by (({apt-listbugs})):
  this is where the package pins are added by the (({apt-listbugs})) program
  and removed by its daily cron job or systemd timer. This file is managed
  automatically and there's normally no need to modify it by hand.

: ((*/var/lib/apt-listbugs/ignore_bugs*))

  Automatic list of bug numbers to be ignored by (({apt-listbugs})): this is
  where the program saves the bug numbers that the user decided to ignore.
  This file is managed automatically and there's normally no need to modify
  it by hand.

: ((*/etc/apt/listbugs/ignore_bugs*))

  User list of bug numbers and packages to be ignored by (({apt-listbugs})):
  this is where the (root) user may manually add bug numbers or package names
  that (({apt-listbugs})) will ignore. This file is only read by
  (({apt-listbugs})), but never modified: the (root) user has to edit it by
  hand. The format is: one bug number or package name per line; lines
  whose first non-blank character is '#' are treated as comments and
  skipped entirely.

: ((*/etc/apt/apt.conf*)) and ((*/etc/apt/apt.conf.d/**))

  Default APT configuration files (see (({apt.conf}))(5) for more details).

: ((*/etc/apt/apt.conf.d/10apt-listbugs*))

  Configuration file fragment for APT containing options related to
  (({apt-listbugs})): this is the recommended place where the (root) user
  may tweak the behavior of (({apt-listbugs})), but usually no customization
  is required.

= AUTHORS

2002 - 2004: (({apt-listbugs})) was originally written by Masato Taruishi
<taru@debian.org>.

2006 - 2008: Junichi Uekawa <dancer@debian.org> rewrote it to handle BTS
Versioning features and the SOAP interface.  The (({--bugs})) option was
added by Francesco Poli in 2008.

2009 - 2010: (({apt-listbugs})) was maintained by Francesco Poli
<invernomuto@paranoici.org> and Ryan Niebur <ryan@debian.org>

2011 - 2012: maintenance was carried on by Francesco Poli and Thomas
Mueller <thomas.mueller@tmit.eu>.

2013 - present day: (({apt-listbugs})) has been maintained by Francesco Poli.

The latest source code is available from
((<"https://salsa.debian.org/frx-guest/apt-listbugs"|URL:https://salsa.debian.org/frx-guest/apt-listbugs>))

= SEE ALSO

(({apt.conf}))(5), (({sensible-browser}))(1), (({xdg-open}))(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\//)
  $VERSION = `dpkg-query -W -f='${Version}' apt-listbugs`
else
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "../lib"))
  $VERSION = `dpkg-parsechangelog -SVersion`
end

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 'aptlistbugs/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"]
  $stderr.puts if $DEBUG
  $stderr.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
  $stderr.puts if $DEBUG
  $stderr.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"
        $stderr.puts "#{pkg}" if $DEBUG
        state=2
      else
        $stderr.print _("E: ") + _("APT Pre-Install-Pkgs failed to provide the expected 'VERSION 3' string.\n")
        exit 1
      end
    when 2
      # APT configuration lines
      case pkg
      when ""
        $stderr.puts "#{pkg}" if $DEBUG
        state=3
      when /^APT::Architecture=(.*)/
        if $1
          $stderr.puts "#{pkg}" if $DEBUG
          native_arch=$1
        end
      when /^quiet=(.*)/
        if $1.to_i > 0
          $stderr.puts "#{pkg}" if $DEBUG
          config.quiet=true
        end
      end
    when 3
      # package action lines
      $stderr.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 provided 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 provided an invalid direction of version change.\n")
          exit 1
        end
      end
    end
  }
  apt_hook_stream.close
  $stderr.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
  $stderr.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
  $stderr.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
