#!/usr/bin/ruby -Ilib/
#
# apt-listbugs: retrieves bug reports and lists them
#
# Copyright (C) 2002  Masato Taruishi <taru@debian.org>
# Copyright (C) 2006-2007  Junichi Uekawa <dancer@debian.org>
#
#  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;
#  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 upgrade/installation

== 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. Especially, it is intended to
be invoked before each upgrade by apt in order to check whether
the upgrade/installation is safe. 

== USAGE

apt-listbugs [-h] [-s <severities>] [-S <stats>] [-D] [-H <hostname>] [-p <port>] [-R] <command> [arguments]

== OPTIONS

* -h | --help

  Print usage help and exit

* -v | --version

  Print version number and exit

* -s <severities> | --severity <severities>

  Severities you want to see separated by comma, possible values are critical, grave, serious, important, normal, minor and wishlist. Default: [critical,grave,serious]
  You can specify '--severity all' to specify all severities.

* -T <tags> | --tag <tags>

  Tags you want to see separated by comma. 

* -S <stats> | --stats <stats>

  Status you want to see separated by comma. Default: [forwarded,done,pending,pending-fixed,]

* -D | --show-downgrade

  Shows bugs of downgraded packages. (apt mode only)

* -H <hostname> | --hostname <hostname>

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

* -p <port> | --port <port>

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

* -P | --pin-priority

  Specifies Pin-Priority value [1000].

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

* -y | --force-yes 

  Assumes that you select yes for all questions.

* -n | --force-no

  Assumes that you select no for all questions.  This option is
  assumed if stdout is not a terminal.

== COMMANDS

* apt

  Reads filenames from standard input (typically provided by apt).

* list [<package1[/version]> <package2[/version]>...]

  Reads package names from the arguments and simply lists bugs of
  these packages. Package versions may be specified with a slash, for
  example, like apt/1.0

* rss [<package1> <package2>...]

  Reads package names from the arguments and lists bugs of these packages
  in rss format.

== 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, soap_use_proxy=on

  If HTTP_PROXY is set and SOAP_USE_PROXY is set, the value is used for HTTP Proxy.
  The default is to use the same value set for apt.

== CONFIGURATION FILE

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

* Acquire::HTTP::Proxy

  The default HTTP Proxy setting.

  Special keyword 'DIRECT' will disable proxy configuration.

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

  HTTP Proxy setting, overrides the default HTTP Proxy setting. Useful
  for setting HTTP proxy for apt-listbugs.



== OUTPUT EXAMPLE

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

  e.g.


  Reading package fields... Done
  Reading package status... Done
  Retrieving bug reports... Done
  Parsing Found/Fixed information... Done
  important bugs of apt-listbugs (0.0.57 -> ) <pending>
   #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 CODE

* 0 

  Success

* 1

  When something is wrong.

* 10 

  To warn APT not to proceed.

== BUGS

Note that apt-listbugs can't probe all the critical bug reports that
really applies to the package of the version. This means that some
bugs are listed because of a conservative reason even if the bugs
don't actually apply to the version. You need to review the bug.

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

Latest source-code is available from
http://git.debian.org/?p=apt-listbugs/apt-listbugs.git

== SEE ALSO

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

=end

$VERSION = "#APT_LISTBUGS_VERSION#"

require 'getoptlong'
require 'debian'
require 'debian/bug'
require 'debian/bts'
require 'thread'
require 'tempfile'
require 'intl'
require 'locale'
require 'rss/maker'

$intl = Intl.new("apt-listbugs")

# ad-hoc
require 'debian/mytempfile'
class HtmlTempfile < MyTempfile
  def _tmpname(basename,tmpdir,n)
    sprintf('%s/%s%d.%d.html', tmpdir, basename, $$, n)
  end
end

class Config

  QUERYBTS = "/usr/bin/querybts"
  WWW_BROWSER = "/usr/bin/www-browser"
  SENSIBLE_BROWSER = "/usr/bin/sensible-browser"

  def usage
    $stderr.print $intl._("Usage: "), File.basename($0),
      $intl._(" [options] <command> [arguments]"),
      "\n",
      $intl._("Options:\n"),
      $intl._(" -h               : Display this help and exit.\n"),
      $intl._(" -v               : Show version number and exit\n"),
      sprintf($intl._(" -s <severities>  : Severities you want to see [%s], or [all].\n"), @severity.join(',')),
      $intl._(" -T <tags>        : Tags you want to see.\n"),
      sprintf($intl._(" -S <stats>       : Stats you want to see [%s].\n"), @stats.join(',')),
      $intl._(" -D               : Show downgraded packages, too.\n"),
      sprintf($intl._(" -H <hostname>    : Hostname of Debian Bug Tracking System [%s].\n"), @hostname),
      sprintf($intl._(" -p <port>        : Port number of the server [%s]\n"), @port),
      sprintf($intl._(" --pin-priority   : Specifies Pin-Priority value [%s]\n"), @pin_priority),
      $intl._(" --title          : Specifies the title of rss output.\n"),
      $intl._(" -f               : Retrieve bug reports from BTS forcibly.\n"),
      $intl._(" -q               : Don't display progress bar.\n"),
      $intl._(" -C <apt.conf>    : Specify apt.conf.\n"),
      $intl._(" -y               : Assume that you select yes for all questions.\n"),
      $intl._(" -n               : Assume that you select no for all questions.\n"),
      $intl._(" -d               : Debug.\n"),
      $intl._("Commands:\n"),
      $intl._(" apt              : apt mode\n"),
      $intl._(" list <pkg...>    : list bug reports of the specified packages\n"), 
      $intl._(" rss <pkg...>     : list bug reports of the specified packages in rss\n"), 
      $intl._("See the manual page for the long options.\n")
  end

  def initialize
    @severity = ["critical", "grave", "serious"]
    @tag = nil
    @stats = ["forwarded", "done", "pending", "pending-fixed", ""]
    @show_downgrade = false
    @hostname = "bugs.debian.org"
    @port = 80
    @quiet = false
    @command = nil

    @parser = nil
    @querybts = nil

    @ignore_bugs = read_ignore_bugs("/etc/apt/listbugs/ignore_bugs")
    @system_ignore_bugs = read_ignore_bugs("/var/lib/apt-listbugs/ignore_bugs")
    @ignore_bugs.each { |bug|
      @system_ignore_bugs.add(bug, false)
    }
    @frontend = ConsoleFrontend.new( self )
    @pin_priority = "1000"
    @apt_conf = nil

    @yes = nil

  end

  attr_accessor :severity, :stats, :quiet, :title
  attr_accessor :show_downgrade, :hostname, :tag
  attr_accessor :frontend, :pin_priority, :yes
  attr_reader :command, :parser, :querybts, :ignore_bugs, :system_ignore_bugs, :browser

  def parse_options
    opt_parser = GetoptLong.new
    opt_parser.set_options(['--help', '-h', GetoptLong::NO_ARGUMENT],
			   ['--severity', '-s', GetoptLong::REQUIRED_ARGUMENT],
			   ['--version', '-v', GetoptLong::NO_ARGUMENT],
			   ['--tag', '-T', GetoptLong::REQUIRED_ARGUMENT],
			   ['--stats', '-S', GetoptLong::REQUIRED_ARGUMENT],
			   ['--show-downgrade', '-D', GetoptLong::NO_ARGUMENT],
			   ['--hostname', '-H', GetoptLong::REQUIRED_ARGUMENT],
			   ['--port', '-p', GetoptLong::REQUIRED_ARGUMENT],
			   ['--pin-priority', '-P', GetoptLong::REQUIRED_ARGUMENT],
			   ['--title', '-E', GetoptLong::REQUIRED_ARGUMENT],
			   ['--quiet', '-q', GetoptLong::NO_ARGUMENT],
			   ['--aptconf', '-C', GetoptLong::REQUIRED_ARGUMENT],
			   ['--force-yes', '-y', GetoptLong::NO_ARGUMENT],
			   ['--force-no', '-n', GetoptLong::NO_ARGUMENT],
			   ['--debug', '-d', GetoptLong::NO_ARGUMENT]
			   );
    
    begin
      opt_parser.each_option { |optname, optargs|
	case optname
	when '--help'
	  usage
	  exit 0
        when '--version'
          puts $VERSION
          exit 0
	when '--severity'
          case optargs
          when "all"
            @severity = ["critical","grave","serious","important","normal","minor","wishlist"]
          else
            @severity = optargs.split(',')
          end
	when '--tag'
	  @tag = optargs.split(',')
	when '--stats'
	  @stats = optargs.split(',')
	when '--show-downgrade'
	  @show_downgrade = true
	when '--hostname'
	  @hostname = optargs
	when '--port'
	  @port = optargs.to_i
	when '--pin-priority'
	  @pin_priority = optargs
	when '--title'
	  @title = optargs
	when '--quiet'
	  @quiet = true
	when '--aptconf'
	  @apt_conf = " -c " + optargs
	when '--debug'
	  $DEBUG = 1
	when '--force-yes'
	  @yes = true
	when '--force-no'
	  @yes = false
	end
      }
    rescue GetoptLong::AmbigousOption, GetoptLong::NeedlessArgument,
	GetoptLong::MissingArgument, GetoptLong::InvalidOption
      usage
      exit 1
    end

    if ! $stdout.isatty
      @quiet = true
      @yes = false if @yes.nil?
    end
 
    @title = "Debian Bugs of #{Socket.gethostname} (#{@severity.join(', ')})" if ! @title

    # http_proxy sanity check
    if ENV["HTTP_PROXY"] != nil && ENV["http_proxy"] == nil
      $stderr.print $intl._("W: sanity check failed: environment variable http_proxy is unset and HTTP_PROXY is set.")
    end
    if ENV["http_proxy"] != nil && ENV["soap_use_proxy"] != "on"
      $stderr.print $intl._("W: sanity check failed: environment variable http_proxy is set and soap_use_proxy is not 'on'.")
    end

    # http_proxy check
    if ENV["http_proxy"] == nil 
      if /http_proxy='(.*)'/ =~ `apt-config #{@apt_conf} shell http_proxy acquire::http::proxy` 
        puts "proxy configuration from APT.CONF: #{$1}" if $DEBUG
        if $1 == 'DIRECT' || $1 == ''
          puts "Disabling proxy due to DIRECT, or empty string" if $DEBUG          
        else
          ENV["http_proxy"] = $1
          ENV["soap_use_proxy"] = "on"
        end
      end
      if /http_proxy='(.*)'/ =~ `apt-config #{@apt_conf} shell http_proxy acquire::http::proxy::bugs.debian.org` 
        if $1 == 'DIRECT' 
          puts "Disabling proxy due to DIRECT" if $DEBUG
          ENV.delete("http_proxy")
          ENV.delete("soap_use_proxy")
        else
          puts "proxy configuration from APT.CONF, specific for bugs.debian.org: #{$1}" if $DEBUG
          ENV["http_proxy"] = $1
          ENV["soap_use_proxy"] = "on"
        end
      end
    end

    # command 
    command = ARGV.shift
    case command
    when nil
      STDERR.puts $intl._("E: You need to specify a command.")
      usage
      exit 1
    when "list"
      @command = "list"
    when "apt"
      @command = "apt"
    when "rss"
      @command = "rss"
    else
      STDERR.puts $intl._("E: Unknown command ") +  "'#{command}'."
      usage
      exit 1
    end

    if @command == "rss"
      @parser =
        Debian::BTS::Parser::SoapIndex.new(@hostname, @port)
    end

    @parser =
      Debian::BTS::Parser::SoapIndex.new(@hostname, @port)	    
    
    if FileTest.executable?("#{QUERYBTS}")
      @querybts = QUERYBTS
    end

    if FileTest.executable?("#{SENSIBLE_BROWSER}")
      @browser = SENSIBLE_BROWSER
    else
      @browser = WWW_BROWSER
    end

  end

  def read_ignore_bugs(path)
    ignore_bugs = IgnoreBugs.new(path)
  end
end

class IgnoreBugs < Array

  @@path_mutex = {}

  def initialize(path)
    super()
    @path = path
    @@path_mutex[path] = Mutex.new if @@path_mutex[path] == nil

    if FileTest.exist?(path)
      open(path).each { |bug|
        if /\s*#/ =~ bug
	  next
        end
        if /\s*(\S+)/ =~ bug
	  self << $1
        end
      }
    end

    @gavewarning = nil
  end

  def add(entry, write=true)
    if write == true
      @@path_mutex[@path].synchronize {
        begin 
          open(@path, "a") { |file|
            file.puts entry
          }
        rescue Errno::EACCES
          # write-access is not possible, ignore it for now.
          if @gavewarning.nil?
            $stderr.puts $intl._("W: Cannot write to bug ignore list file")
            @gavewarning = true
          end
        end
      }
    end
    self << entry
  end

end

class Viewer

  def initialize(config)
    @config = config
  end

  class SimpleViewer < Viewer

    DeprecatedWarning = $intl._("********** on_hold IS DEPRECATED. USE p INSTEAD to use pin **********")
    DeprecatedWarningHeader = "*" * DeprecatedWarning.length

    def view(new_pkgs, cur_pkgs, bugs)
      if display_bugs(bugs, new_pkgs.keys, cur_pkgs, new_pkgs) == false
        return true
      end

      if @config.command == "list"
	return true
      end

      answer = "n"
      hold_pkgs = []
      while true
	ask_str = $intl._("Are you sure you want to install/upgrade the above packages?")
	if @config.querybts != nil || @config.browser != nil
	  if hold_pkgs.empty?
	    ask_str << " [Y/n/?/...] "
	  else
	    ask_str << "[N/?/...] "
	  end
	else
	  ask_str << " [Y/n] "
	end
	if @config.yes.nil?
          a = @config.frontend.ask ask_str
        else
          a = "y" if @config.yes
          a = "n" if ! @config.yes
        end
	if a == ""
	  if hold_pkgs.empty?
	    answer = "y"
	  else
	    answer = "n"
	  end
	else
	  answer = a.downcase
	end
	case answer
	when "y"
	  if hold_pkgs.empty?
            bugs.each { |bug|
              if ! @config.system_ignore_bugs.include?(bug.bug_number)
                @config.system_ignore_bugs.add(bug)
                @config.system_ignore_bugs.add(bug.bug_number)
              end
            }
	    return true
	  end
	when  "n"
	  return false
	when /^(\d+)$/
	  if @config.querybts != nil
	    system("#{@config.querybts} #{$1} < /dev/tty")
          else
            @config.frontend.puts sprintf($intl._("You must install the reportbug package to be able to do this"))
          end
	when /^i\s+(\d+)$/
	  if ! @config.system_ignore_bugs.include?($1)
	    @config.system_ignore_bugs.add($1)
	    Factory::BugsFactory.delete_ignore_bugs(bugs)
	    @config.frontend.puts sprintf($intl._("%s ignored"), $1)
	  else
	    @config.frontend.puts sprintf($intl._("%s already ignored"), $1)
	  end
	when "r"
	  display_bugs(bugs, new_pkgs.keys - hold_pkgs, cur_pkgs, new_pkgs)

	when /^(h|p)\s+(.+)$/
	  key = $1
	  if key == "h"
	    @config.frontend.puts DeprecatedWarningHeader
	    @config.frontend.puts DeprecatedWarning
	    @config.frontend.puts DeprecatedWarningHeader
	  end
	  pkgs = $2.split(/\s+/)
	  if key == "h"
	    h = on_hold(pkgs)
	  else
	    h = pinned(pkgs, cur_pkgs, bugs)
	  end
	  hold_pkgs.concat(h) if h != nil

	when "w"
	  puts bugs if $DEBUG
	  display_bugs_as_html(bugs, cur_pkgs.keys - hold_pkgs, cur_pkgs, new_pkgs) if @config.browser != nil

        when /(h|p)/
	  key = $1
	  if key == "h"
	    @config.frontend.puts DeprecatedWarningHeader
	    @config.frontend.puts DeprecatedWarning
	    @config.frontend.puts DeprecatedWarningHeader
	  end
	  pkgs = {}
	  if key == "p"
	    bugs.each { |bug|
	      ## FIXME: need to parse preferences correctly?
	      if ! system("grep -q \"Package: #{bug.pkg_name}\" /etc/apt/preferences 2> /dev/null")
	        pkgs[bug.pkg_name] = 1
	      end
            }
	  else
            bugs.each { |bug|
              pkgs[bug.pkg_name] = 1
            }
	  end
	  if pkgs.size != 0
	    if @config.frontend.yes_or_no? sprintf($intl._("The following %s packages will be pinned or on hold:\n %s\nAre you sure "), pkgs.size, pkgs.keys.join(', '))
	      if key == "h"
                h = on_hold(pkgs.keys)
              else
                h = pinned(pkgs.keys, cur_pkgs, bugs)
              end
            end
	    hold_pkgs.concat(h) if h != nil
	  else
	    @config.frontend.puts sprintf($intl._("Every packages already pinned or on hold. Ignoring %s command."), key)
	  end
	else
	  if hold_pkgs.empty?
	    @config.frontend.puts $intl._("     y     - continue the apt installation.\n")
	  end
	  @config.frontend.puts "" +
	    $intl._("     n     - stop the apt installation.\n") +
	    $intl._("   <num>   - query the specified bug number (requires reportbug).\n") +
	    $intl._("     r     - redisplay bug lists.\n") +
	    $intl._(" p <pkg..> - make pkgs pinned: need to restart apt to enable.\n") +
	    $intl._(" p         - make all the above pkgs pinned. need to restart.\n") +
	    $intl._(" i <num>   - make bug_number <num> ignored.\n") + 
	    $intl._("     ?     - print this help.\n")
	  if @config.browser != nil
	    @config.frontend.puts sprintf($intl._("     w     - display bug lists in html (uses %s).\n"), File.basename(@config.browser))
	  end
	end
      end
    end

    def bugs_of_pkg( bugs, pkg )
      b = []
      bugs.each { |bug|
        b << bug if bug.pkg_name == pkg
      }
      b
    end

    def pinned(pkgs, cur_pkgs, bugs)
      holdstr = ""
      pkgs.each { |pkg|
        if cur_pkgs[pkg] == nil
	  @config.frontend.puts sprintf($intl._("Package '%s' is newly installed, ignored"), pkg)
	  next
	end
	holdstr << "
Explanation: Pinned by apt-listbugs at #{Time.now}"
        bugs_of_pkg( bugs, pkg ).each { |bug|
	  holdstr << "
Explanation:   ##{bug.bug_number}: #{bug.desc.gsub("'","\\'")}"
        }
	holdstr << "
Package: #{pkg}
Pin: version #{cur_pkgs[pkg]['version']}
Pin-Priority: #{@config.pin_priority}
"
      }
      $stderr.puts holdstr if $DEBUG
      if holdstr != ""
        File.open("/etc/apt/preferences", "a") { |io|
          io.puts holdstr
	  @config.frontend.puts sprintf($intl._("%s pinned by adding Pin preferences in /etc/apt/preferences. You need to restart apt to enable"), pkgs.join(' '))
	  return pkgs
        }
      end
      return nil
    end

    def on_hold (pkgs)
      holdstr = ""
      pkgs.each { |pkg|
        holdstr << "#{pkg} hold\n"
      }
      if system("echo '#{holdstr}' | dpkg --set-selections")
        @config.frontend.puts sprintf($intl._("%s held: you need to restart apt to enable"), pkgs.join(' '))
        return pkgs
      end
      return nil
    end

    def display_bugs(bugs, pkgs, cur_pkgs, new_pkgs)
      # routine to display every bug that is available and relevant

      p_bug_numbers = []
      bugs_statistics = {}
      @config.stats.each { |stat|
	@config.severity.each { |severity|
	  pkgs.each { |pkg|
	    bug_exist = 0
	    bugs_statistics[pkg] = 0 unless bugs_statistics[pkg]
	    bugs.each_by_category(pkg, severity, stat) { |bug|
	      next if p_bug_numbers.include?(bug.bug_number)
	      bugs_statistics[pkg] += 1
	      p_bug_numbers << bug.bug_number
	      if bug_exist == 0
		buf = sprintf($intl._("%s bugs of %s ("), severity, pkg)
		buf += "#{cur_pkgs[pkg]['version']} " if cur_pkgs[pkg] != nil
		buf += "-> #{new_pkgs[pkg]['version']}) <#{bug.stat}>"
		@config.frontend.puts buf
		bug_exist = 1
	      end
	      bug_str = " ##{bug.bug_number} - #{bug.desc}"
              bug_str += sprintf($intl._(" (Found: %s)"), "#{bug.found}") if ( ! bug.found.nil? ) && $DEBUG
              bug_str += sprintf($intl._(" (Fixed: %s)"), "#{bug.fixed}") if ! bug.fixed.nil?
	      @config.frontend.puts bug_str
	      if bug.mergeids.size > 0
		bug_str =  $intl._("   Merged with:")
		bug.mergeids.each { |m|
		  bug_str << " #{m}"
		  p_bug_numbers << m
		}
		@config.frontend.puts bug_str
	      end
	    }
          }
        }
      }
      stat_str_ary = []
      bugs_statistics.each { |pkg, num|
	if num > 0
	  if num > 1
	    buf = sprintf($intl._("%s(%s bugs)"), pkg, num)
	  else
	    buf = sprintf($intl._("%s(%s bug)"), pkg, num)
	  end
	  stat_str_ary << buf
	end
      }
      if stat_str_ary.size > 0
	@config.frontend.puts $intl._("Summary:\n ") + stat_str_ary.join(', ')
	return true
      else
        return false
      end
    end

    def each_state_table(o, bugs, stats)
      stats.each { |stat|
	sub = bugs.sub("stat", stat)
	if sub.size > 0
	  o.puts "<table border=2 width=100%>"
	  o.puts sprintf($intl._(" <caption>Bug reports which are marked as %s "), stat) + 
	    $intl._("in the bug tracking system</caption>")
	  o.puts $intl._(" <tr><th>package</th><th>severity</th><th>bug number</th><th>description</th></tr>")	  
	  yield sub
	  o.puts "</table><br>"
	end
      }
    end

    def display_bugs_as_html(bugs, pkgs, cur_pkgs, new_pkgs)
      bug_exist_for_stat = 0
      bug_exist_for_pkg = 0
      bug_exist = 0
      displayed_pkgs = []

      tmp = HtmlTempfile.new("apt-listbugs")
      tmp.puts "<html><head><title>"+$intl._("critical bugs for your upgrade")+"</title><meta http-equiv=\"Content-Type\" content=\"text/html; charset=#{Locale::LangInfo.langinfo(Locale::LangInfo::CODESET)}\"></head><body>"
      tmp.puts $intl._("<h1 align=\"center\">Critical bugs for your upgrade</h1>")
      tmp.puts $intl._("<p align=\"right\">by apt-listbugs</p><hr>")
      tmp.puts $intl._("<h2>Bug reports</h2>")

      each_state_table(tmp, bugs, @config.stats) { |bugs|
	bugs.each { |bug|
	  tmp.puts "<tr><td>#{bug.pkg_name}</td><td>#{bug.severity}</td><td><a href=\"http://bugs.debian.org/cgi-bin/bugreport.cgi?archive=no&bug=#{bug.bug_number}\">##{bug.bug_number}</a></td><td>#{bug.desc}</td></tr>"
	  displayed_pkgs << bug.pkg_name if !displayed_pkgs.include?(bug.pkg_name)
        }
      }

      tmp.puts $intl._("<h2>Package upgrade information in question</h2>")
      tmp.puts "<ul>"
      displayed_pkgs.each { |pkg|
	tmp.puts "<li>#{pkg}("
	tmp.puts "#{cur_pkgs[pkg]['version']} " if cur_pkgs[pkg] != nil
	tmp.puts "-&gt; #{new_pkgs[pkg]['version']}" if new_pkgs[pkg] != nil
	tmp.puts ")"
      }
      tmp.puts "</ul>"

      tmp.puts "</body></html>"
      tmp.close

      puts "Invoking www-browswer for #{tmp.path}" if $DEBUG
      browsercommandline = ""
      browsercommandline << "su #{ENV["SUDO_USER"]} -c \"" if ENV["SUDO_USER"]
      browsercommandline << "#{@config.browser} #{tmp.path} < /dev/tty"
      browsercommandline << "\"" if ENV["SUDO_USER"]
      if system(browsercommandline)
        puts "successfully invoked www-browswer" if $DEBUG
      else
        $stderr.puts $intl._("W: Failed to invoke www-browser.")
        $stderr.puts " #{browsercommandline}"
      end
    end
  end


  class RSSViewer < Viewer

    def initialize(config)
      super(config)
    end

    def encode(str)
      buf = str.gsub("<", "&lt;")
      buf
    end

    def view(new_pkgs, cur_pkgs, bugs)
      rss = RSS::Maker.make("2.0") { |maker|
        maker.channel.about = ""
        maker.channel.title = @config.title
        maker.channel.description = @config.title
        maker.channel.link = "http://bugs.debian.org/"

        bugs.each { |bug|
          if @config.stats.include?( bug.stat )
            item = maker.items.new_item
            item.link = "http://bugs.debian.org/cgi-bin/bugreport.cgi?archive=no&amp;bug=#{bug.bug_number}"
	    item.title = encode("Bug##{bug.bug_number}: #{bug.desc}")
	    item.date = Time.parse("#{bug.time.year}/#{bug.time.month}/#{bug.time.day} #{bug.time.hour}:#{bug.time.min}:#{bug.time.sec}")

            buf = ""

            buf << "<ul>\n"
            buf << "<li>Bug##{bug.bug_number}</li>\n"
            buf << "<li>Package: #{bug.pkg_name}</li>\n"
            buf << "<li>Severity: #{bug.severity}</li>\n"
            buf << "<li>Status: #{bug.stat}</li>\n"
            buf << "<li>Tags: #{bug.tags.join(',')}</li>\n" if bug.tags != nil

            if bug.mergeids.size > 0
              buf << "<li>Merged with:\n"
              bug.mergeids.each { |id|
                url  = "http://bugs.debian.org/cgi-bin/bugreport.cgi?archive=no&amp;bug=#{id}"
                buf << "<a href=\"#{url}\">#{id}</a>"
              }
            end
            buf << "</li>\n"
            buf << "</ul>\n"

            item.description = buf

	  end
        }

      }
      @config.frontend.puts rss.to_s
    end
    
  end

end


module Factory

  Done = $intl._("Done") 
  CONCURRENCY_LEVEL = 3

  def done?(done)
    Done == done
  end

  def config
      @@config
  end

  def config=(c)
      @@config = c
  end

  def create(arg, *args)
    raise $intl._("Not Implemented")
  end

  module_function :config, :config=, :create, :done?
  public :create

  module PackageFactory
    extend Factory

    # parse 'apt-listbugs list pkgname/version .... ' combination.
    module ListFactory
      include Factory
      def field(pkgname)
	f = {}
        case pkgname
        when /^(.*)\/(.*)$/
          f["package"] = $1
          f["version"] = $2
        else
          f["package"] = pkgname
        end
	f
      end
      module_function :field
    end

    module AptFactory
      include Factory
      DPKG = "/usr/bin/dpkg"
      def field(pkgpath)
        # TODO: This is slow
	# field = Debian::Dpkg.field(pkg)
        #  ^- this does not work due to bug in Debian::Dpkg Bug#390262
	field = {}
	f =  `#{DPKG} -I #{pkgpath}`
	f.each { |line|
	  case line
	  when /^ Package: (.*)/
	    field["package"] = $1
	  when /^ Version: (.*)/
	    field["version"] = $1
	  end
	}
	field
      end
      module_function :field
    end

    def create(pkgnames, *args)
      # obtain package names and get information about them
      step = 100.0 / pkgnames.size.to_f
      pkgs = {}
      reading = $intl._("Reading package fields...")
      pkgnames.each_index { |index|
	if block_given?
	  yield reading, (index * step).to_i.to_s + "%"
	end
	pkgnames[index].chomp!
	f = nil
	case config.command
	when "apt"
	  f = AptFactory.field(pkgnames[index])
	when "list"
	  f = ListFactory.field(pkgnames[index])
	when "rss"
	  f = ListFactory.field(pkgnames[index])
	else
	  raise $intl._("Not Implemented")
	end
	pkgs[f["package"]] = f
      }
      if block_given?
	yield reading, Done
      end
      pkgs
    end
    
    def delete_ignore_pkgs(new_pkgs)
      new_pkgs.delete_if { |name, pkg|
	config.system_ignore_bugs.include?(name)
      }
    end

    module_function :create, :delete_ignore_pkgs

  end

  module StatusFactory
    extend Factory

    ReadStatusMsg = $intl._("Reading package status...")

    def create(pkgs, *args)
      # creating status database, which contains the list of packages.

      if block_given?
	yield ReadStatusMsg, "0%"
      end
      pkgres = []
      pkgs.each_key { |pkg|
        pkgres << "#{pkg}"
      }      
      i=0
      max=pkgres.size
      step=(pkgres.size/100)*10+1
      status = Debian::Packages.new(Debian::Dpkg::STATUS_FILE, pkgres) do
	yield ReadStatusMsg, 
          "#{(i.to_f/max.to_f*100).to_i}%" if (i % step) == 0
	i += 1
      end
      if block_given?
	yield ReadStatusMsg, Done
      end
      status
    end

    def delete_downgraded(cur_pkgs, new_pkgs)
      new_pkgs.delete_if { |name, pkg|
	val = false
	ver = pkg["version"]
	cur_ver = cur_pkgs[name]["version"] if cur_pkgs[name] != nil
	if ver != nil && cur_ver != nil
	  val = true if Debian::Dpkg.compare_versions(ver, "lt", cur_ver)
	end
	val
      }
    end

    module_function :create, :delete_downgraded

  end

  module BugsFactory
    extend Factory

    RetrvBTSMsg = $intl._("Retrieving bug reports...")

    def create(new_pkgs, *args, &progress)
      cur_pkgs = args[0]
      bugs = Debian::Bugs.new
      pkg_step = 100 / new_pkgs.size.to_f
      retrycount = 10 # retry 10 times

      size = new_pkgs.size
      mutex = Mutex.new
      threads = []
      yield RetrvBTSMsg, "0%"
      begin
        # obtain a list of package names
        tmppkgs = []
        new_pkgs.each_key { |k| tmppkgs << k }
        
        # send the list of package names and severity to be parsed.
        bugs = config.parser.parse(tmppkgs, config.severity) { |pct|
          yield RetrvBTSMsg, pct
        }
      rescue SOAP::HTTPStreamError => exception
        config.frontend.puts $intl._(" Fail")
        config.frontend.puts " Exception: " + exception.class.to_s if $DEBUG
        $stderr.puts $intl._(" E: HTTP GET failed")
        retrycount -= 1
        retry if config.frontend.yes_or_no?($intl._("Retry downloading bug information?")) && retrycount > 0
        raise $intl._("Exiting with error") if config.frontend.yes_or_no?($intl._("Abort the installation"))
        bugs = []
      rescue SOAP::EmptyResponseError => exception
        config.frontend.puts $intl._(" Fail")
        config.frontend.puts " Exception: " + exception.class.to_s if $DEBUG
        $stderr.puts $intl._(" E: Empty stream from SOAP")
        retrycount -= 1
        retry if config.frontend.yes_or_no?($intl._("Retry downloading bug information?")) && retrycount > 0
        raise $intl._("Exiting with error") if config.frontend.yes_or_no?($intl._("Abort the installation"))
        bugs = []
      rescue => exception
        config.frontend.puts $intl._(" Fail")
        config.frontend.puts " Exception: " + exception.class.to_s if $DEBUG
        config.frontend.puts $intl._("Error retrieving bug reports from the server with the following error message:")
        config.frontend.puts " W: #{$!}"
        if exception.kind_of? SocketError
          config.frontend.puts $intl._("It appears that your network connection is down. Check network configuration and try again") 
        else
          config.frontend.puts $intl._("It could be because your network is down, or because of broken proxy servers, or the BTS server itself is down. Check network configuration and try again") 
        end
        retrycount -= 1
        retry if config.frontend.yes_or_no?($intl._("Retry downloading bug information?")) && retrycount > 0
        raise $intl._("Exiting with error") if config.frontend.yes_or_no?($intl._("Abort the installation"))
        bugs = []
      end
      yield RetrvBTSMsg, "100%"
      
      if block_given?
        yield RetrvBTSMsg, Done
      end
      bugs
    end

    def delete_ignore_bugs(bugs)
      # ignoring ignore_bugs
      bugs.delete_if { |bug| config.system_ignore_bugs.include?(bug.bug_number)}
    end

    def iterate_fixed_found_version(space_delimited_bts_version, pkg_name)
      # iterate relevant versions
      if space_delimited_bts_version.nil?
        return;
      end
      space_delimited_bts_version.split(" ").each { |version|
        # check each fixed_version
        case version
        when /^(.*)\/(.*)$/
          if $1 == pkg_name # TODO: actually, this need to be source_name
            yield $2
          else
            # TODO: ignore this until I figure out how to get source_name instead of pkg_name
            #fixed_ver=nil
            yield $2
          end
        else
          yield version
        end
      }
    end      

    def find_max_version_below_new_ver(space_delimited_bts_version, new_ver, pkg_name)
      # find the max version from found/fixed that is below new_ver
      # data format of space_delimited_bts_version:
      # space-delimited PACKAGE/VERSION, or VERSION.

      fixed_max = nil 
      iterate_fixed_found_version(space_delimited_bts_version, pkg_name) { |fixed_ver|
        # check each fixed_ver
        if Debian::Dpkg.compare_versions(fixed_ver, "le", new_ver) &&
            ( fixed_max == nil || Debian::Dpkg.compare_versions(fixed_max, "le", fixed_ver) )
          fixed_max = fixed_ver
        end
      }
      fixed_max
    end

    BugFixedParseMsg = $intl._("Parsing Found/Fixed information...")

    def delete_threshold_bugs (bugs, cur_pkgs, new_pkgs)
      # Ignore bugs that do not apply to the installing version.

      max=bugs.size
      step=(max/100)*10+1
      i=0
      yield BugFixedParseMsg, "0%"
      
      bugs.delete_if { |bug|
	val = false
	name = bug.pkg_name
	new_ver = nil
	cur_ver = nil
	new_ver = new_pkgs[name]["version"] if new_pkgs[name] != nil
	cur_ver = cur_pkgs[name]["version"] if cur_pkgs[name] != nil
        
        remove_newer_and_irrelevant_bugs = true 

        # show progress
        yield BugFixedParseMsg, 
        "#{(i.to_f/max.to_f*100).to_i}%" if (i % step) == 0
        i += 1

        # ignore bugs that have no fixed version info, and is closed with a XXXX-done
        if bug.fixed.nil? && bug.stat == "done"
          puts "bug_number:##{bug.bug_number} -- apparently closed with XXXX-done without version info" if $DEBUG
          val = true
          ##316121
        end
        
        if new_ver.nil? 
          # ignore if I don't have the version number for this package. 
          # only happens with 'apt-listbugs list'
          puts "bug_number:##{bug.bug_number} --- since it's [apt-listbugs list], I don't know what version is to be installed; assuming #{cur_ver}" if $DEBUG

          # TODO: doing this is possible, but not always useful since too-new bugs are removed, maybe make it optional.
          new_ver = cur_ver
          remove_newer_and_irrelevant_bugs = false
        end
        
        if !new_ver.nil?
          fixed_max = nil
          found_max = nil
          
          fixed_max = find_max_version_below_new_ver(bug.fixed, new_ver, name) if ! bug.fixed.nil?
          found_max = find_max_version_below_new_ver(bug.found, new_ver, name) if ! bug.found.nil?
          
          greater_found_at_all = false
          less_found_at_all = false
          iterate_fixed_found_version(bug.found, name) { |found|
            less_found_at_all = false if Debian::Dpkg.compare_versions(found, "le", new_ver)
            greater_found_at_all = true if Debian::Dpkg.compare_versions(found, "gt", new_ver)
          }
          
          # eliminate bugs that are found later than this package
          val = true if greater_found_at_all && ! less_found_at_all && remove_newer_and_irrelevant_bugs
          puts "bug_number:##{bug.bug_number} greater:#{greater_found_at_all} less:#{less_found_at_all} delete too new bug:#{val}" if $DEBUG
          
          if fixed_max == nil
            # this bugreport is not fixed
            puts " ... fixed_max is nil: this means this bug report is fixed later than the version installed, or filed before stone-age" if $DEBUG
          else if  found_max == nil 
                 # this bugreport is not found or too old to be found, don't know how to handle.
                 puts " ... found_max is nil: this means this bug report is found later than the version installed, or filed before stone-age" if $DEBUG
                 if fixed_max != nil 
                   val = true
                   puts " ... but this bug report has fixed information, assume it is fixed" if $DEBUG
                 end
               else if Debian::Dpkg.compare_versions(fixed_max, "gt", found_max)
                      # this bugreport has recently been fixed
                      puts " ... fixed_max > found_max: assuming that this bug is fixed for this version" if $DEBUG
                      val = true
                    end
               end
          end
          puts "in conclusion, For bug #{bug.bug_number} comparing fixed: [#{bug.fixed}]->#{fixed_max}, found:[#{bug.found}]->#{found_max}, and new_ver:#{new_ver} results in removal:#{val} " if $DEBUG
        end
        val
      }
      yield BugFixedParseMsg, "100%"
      yield BugFixedParseMsg, Done
      bugs
    end

    def delete_unwanted_tag_bugs( bugs )
      puts "checking unwanted bugs: #{bugs}" if $DEBUG
      bugs.delete_if { |bug|
        val = false
        puts "#{bug}" if $DEBUG
        config.tag.each { |tag|
	  if bug.tags && bug.tags.include?( tag )
            puts "#{bug} has {tag}" if $DEBUG
	  else
	    val = true
	  end
	}
	val
      }
    end
    
    module_function :create, :delete_ignore_bugs,
    :delete_threshold_bugs, :delete_unwanted_tag_bugs, 
    :find_max_version_below_new_ver, 
    :iterate_fixed_found_version
  end

end

class ConsoleFrontend

  def initialize( config )
    @tty = nil
    @old = ""
    @config = config
  end

  def progress(msg, val)
    $stderr.print "\r"
    $stderr.print " " * @old.length
    $stderr.print "\r"
    @old = "#{msg} #{val}"
    $stderr.print @old
    $stderr.flush
    $stderr.puts "" if Factory.done?(val)
  end

  def puts(msg)
    $stdout.puts msg
  end
  
  def ask(msg)
    $stdout.print "#{msg} "
    $stdout.flush
    line = nil
    @tty = open("/dev/tty") if @tty == nil
    line = @tty.gets
    if line != nil
      line.chomp!
    end
    return line
  end

  def yes_or_no?(msg, default = true)
    return @config.yes if ! @config.yes.nil?
    while true
      msgyn = "#{msg}"
      if default == true
	msgyn << "[Y/n]?"
      else
	msgyn << "[y/N]?"
      end
      a = ask msgyn
      if a == ""
	return default
      elsif a == "Y" || a == "y"
	return true
      elsif a == "N" || a == "n"
	return false
      end
    end
  end

  def close
    @tty.close if @tty
  end

end

## main from here

trap("INT") { $stderr.puts "Interrupted"; exit(0) }

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

# handle arguments
pkgnames = []
holdpkgs = {}
case config.command
when "apt"
  # parse apt VERSION 2 input.
  state=1
  STDIN.each { |pkg|
    pkg=pkg.rstrip
    case state
    when 1
      # the version header, only one line.
      if pkg == "VERSION 2"
        state=2
      else
        $stderr.print $intl._("E: apt Pre-Install-Pkgs is not giving me expected 'VERSION 2' string.\n")
        exit 1
      end
    when 2
      # apt configuration lines
      case pkg
      when ""
        state=3
      when /^quiet=(.*)/
        if $1.to_i > 0 
          config.quiet=true
        end
      end
    when 3
      # package filenames
      filename=pkg.split(" ")[4]
      case filename
      when "**CONFIGURE**"
        # none
      when "**REMOVE**"
        # none
      else
        pkgnames << filename
      end
    end
  }
when "list"
  ARGV.each { |pkg|
    # pkg may be frozen string
    pkgnames << "#{pkg}"
  }
when "rss"
  ARGV.each { |pkg|
    pkgnames << "#{pkg}"
  }
end

exit 0 if pkgnames.size == 0

# creating new packages database
new_pkgs = Factory::PackageFactory.create(pkgnames) { |msg, val|
  config.frontend.progress(msg, val) if config.quiet == false
}

Factory::PackageFactory.delete_ignore_pkgs(new_pkgs) if config.command == "apt"
# exitting if no new packages is found
exit 0 if new_pkgs.size == 0

# creating current packages database
cur_pkgs = Factory::StatusFactory.create(new_pkgs) { |msg, val|
  config.frontend.progress(msg, val) if config.quiet == false
}
if ( config.show_downgrade == false ) and ( config.command == "apt" )
  Factory::StatusFactory.delete_downgraded(cur_pkgs, new_pkgs)
end

# reading bug reports
begin
  bugs = Factory::BugsFactory.create(new_pkgs, cur_pkgs) { |msg, val|
    config.frontend.progress(msg, val) if config.quiet == false
  }
rescue
  config.frontend.puts " ... E: #{$!}"
  exit 10
end

Factory::BugsFactory.delete_ignore_bugs(bugs) if config.command == "apt"
Factory::BugsFactory.delete_unwanted_tag_bugs(bugs) if config.tag
begin
  Factory::BugsFactory.delete_threshold_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 =  $intl._("****** Exit with an error by force in order to stop the installation. ******")
  ErrorWarningHeader = "*" * ErrorWarning.length
  config.frontend.puts ErrorWarningHeader
  config.frontend.puts ErrorWarning
  config.frontend.puts ErrorWarningHeader
  config.frontend.close
  exit 10
end
config.frontend.close
