#!/usr/local/bin/ruby
# PortsView: Query Tool for the OpenBSD sqlports database
#
# This is a ruby cgi script to display various features of the OpenBSD sqlports database
# The script should be installed as /var/www/cgi-bin/portsview
# Also install bsd.ports.mk.html in /var/www/htdocs
#
# An extended version of the database should be installed in /usr/local/share/portsview/sqlports2.db
#
# Note: two enhancements to the database are required for this script:
# system_libs table (fullpkgpath, libname, type)
# category column for the ports table, with the name of the port category
# The script will create hyperlinks between different ports where possible.
#
# The script also depends on man2web conversion of bsd.port.mk to provide hyperlinks for definitions.
#
# October 12, 2006
# Author: "Frederick C. Druseikis <fredd@cse.sc.edu>"
# Copyright 2006, Frederick C. Druseikis
# License: newBSD
# vim: ft=ruby

#-----------------------------------------------------------------------------------
# set this to the location of the extended sqlports database
SQLPORTS_PATH = "/usr/local/share/portsview/sqlports2.db"
PORTSVIEW_VERSION = "0.2"
#-----------------------------------------------------------------------------------

require "cgi"

# extensions to defined cgi class
class CGI

  # makes html readable output
  def out_pretty
    if block_given? then
      self.out { CGI::pretty(yield) }
    else
    end
  end

  # not used
  def openbsd39_logo
    self.a('http://www.openbsd.org') { self.img( 'SRC' => 'http://www.openbsd.org/images/puffy39.jpg' ) }
  end

  # logo at the top of each page
  def openbsd_logo
    self.a('http://www.openbsd.org') {
	  self.img( 'ALT' => '[openbsd]', 'SRC' => 'http://www.openbsd.org/images/smalltitle.gif', 'BORDER' => '0' )
    }
  end

  # returns URL of the package page for the given item
  def package_href(item,show_all)
    path = (item == nil ? 'mystuff/no-such-package' : CGI::escape(item))
    "/cgi-bin/portsview?mode=package&fullPkgPath=#{path}&showAll=#{show_all}"
  end

  # returns URL of the category page for the given item
  def category_href(item,show_all)
    "/cgi-bin/portsview?mode=category&category=#{item}&showAll=#{show_all}"
  end

  # mixin to deliver an array in chunks, for formatting tables
  def array_grouper(array,width)
    if block_given? then
      ( 0 .. (array.size/width) ).to_a.inject("") do |result,n|
        result + yield(array[ (n*width) ... (n*width+width) ])
      end
    end
  end

end

cgi = CGI.new("html4Tr")
fullPkgPath = cgi['fullPkgPath']
showAll = cgi['showAll'] == "true"
category = cgi['category']
mode = cgi['mode']

# OpenBSD categories under /usr/ports
categories = [
  ['archivers', 'astro', 'audio', 'benchmarks', 'biology', ],
  ['books', 'cad', 'chinese', 'comms', 'converters', ],
  ['databases', 'devel', 'distfiles', 'editors', 'education', ],
  ['emulators', 'games', 'graphics', 'infrastructure', 'japanese', ],
  ['java', 'korean', 'lang', 'mail', 'math', ],
  ['mbone', 'misc', 'multimedia', 'net', 'news', ],
  ['palm', 'plan9', 'print', 'productivity', 'russian', ],
  ['security', 'shells', 'sysutils', 'telephony', 'textproc', ],
  ['www', 'x11' ]
]

def mySignature(cgi)
  cgi.hr +
  cgi.p {
    cgi.small {
      "OpenBSD PortsView #{PORTSVIEW_VERSION} by Frederick C. Druseikis (fredd at cse dot sc dot edu)" 
    }
  } +
  cgi.p {
    ENV['SERVER_SIGNATURE']
  }
end

if mode == "" then

# the default web page

  cgi.out_pretty{
    cgi.html{
      cgi.head{
        cgi.title{"Query the OpenBSD sqlports database"}
      } +
      cgi.body{
        cgi.openbsd_logo +
        cgi.h1 { "OpenBSD PortsView" } +
        cgi.hr +
        cgi.h2 { "Lookup by Full Package Path" } +
		cgi.p {
		  "Drill down to the detailed Makefile settings for a package." +
		  "Enter the full package name with flavors, e.g., something like <tt>archivers/zip</tt>" +
          "<tt>devel/gstreamer,-main</tt> or <tt>devel/eclipse/sdk,-main</tt>"
		} +
        cgi.form( 'METHOD' => 'get' ) {
		  cgi.hidden('mode','package') +
          cgi.text_field("fullPkgPath") +
          cgi.checkbox('showAll','true',true) + " Show All Variables" +
          cgi.br +
          cgi.submit
        } +
		cgi.br +
		cgi.hr +
		cgi.h2 { "String Search" } +
		cgi.p {
		  "Enter a keyword and select one or more fields to search."
		} +
		cgi.form {
		  cgi.hidden('mode','search') +
		  cgi.text_field("keyWord") +
		  cgi.br +
          cgi.checkbox('searchPath','true',false) + " Package Names" +
          cgi.checkbox('searchMaintainer','true',true) + " Maintainers" +
          cgi.checkbox('searchDescr','true',false) + " Descriptions" +
          cgi.checkbox('searchComment','true',false) + " Comments" +
          cgi.checkbox('searchSharedLib','true',false) + " Shared Libs" +
          cgi.checkbox('searchWantLib','true',false) + " Want Libs" +
		  cgi.br +
		  cgi.submit
		} +
		cgi.br +
        cgi.hr +
        cgi.h2 { "Packages by Category" } +
        cgi.table( 'CELLPADDING' => '4', 'BORDER' => '0'  ) { 
          categories.inject("") { |result,grouping|
            result +
            cgi.tr {
              grouping.inject("") { |result,category|
                result +
                cgi.td( 'WIDTH' => '100px' ) {
                  cgi.a(cgi.category_href(category,showAll)) {category}
                }
              }
            }
          }
        } +
		mySignature(cgi)
      }
    }
  }

else # everything else is the result of a query

require 'rubygems'
require_gem 'sqlite3-ruby'

# this code requires three patches to the sqlports database:
#   -- categories table
#   -- category column on the ports table
#   -- system_libs table

db = SQLite3::Database.new( SQLPORTS_PATH )

if mode == 'category' then # a category query: list all the packages under that name

  packages = db.execute( "select fullpkgpath from ports where category = ?", category )

if packages == nil || packages.length == 0 then
  cgi.out_pretty {
    cgi.html {
      cgi.body {
        cgi.h1 { category } +
        cgi.p {
          "No packages were found in this category."
        }
      }
    } 
  }
else
  cgi.out_pretty {
    cgi.html {
      cgi.head {
        cgi.title { "Package Selection" }
      } +
      cgi.body {
        cgi.openbsd_logo +
        cgi.h1 { "Packages in the <i>#{category}</i> Category" } +
        cgi.hr +
        cgi.table( 'CELLPADDING' => '4', 'BORDER' => '2' ) {
          cgi.array_grouper(packages,5) { |items|
            cgi.tr {
              items.inject("") { |result,item|
                result + cgi.td {
                  cgi.a(cgi.package_href(item[0],showAll)) { a = item[0].split('/'); a[1 ... a.size].join('/') }
                }
              }
            }
          }
        } +
        mySignature(cgi)
      }
    }
  }
end

elsif mode == 'search' then # an imitation full text search under a few selected columns

  selectHash = {
    'Path' => "select fullpkgpath, fullpkgpath from ports where fullpkgpath like ?",
    'Descr' => "select fullpkgpath, value from descr where value like ?",
    'Comment' => "select fullpkgpath, comment from ports where comment like ?",
    'Maintainer' => "select fullpkgpath, maintainer from ports where maintainer like ?",
    'WantLib' => "select fullpkgpath, wantlib from ports where wantlib like ?",
    'SharedLib' => "select fullpkgpath, shared_libs from ports where shared_libs like ?"
  }

  resultsHash = Hash.new(nil)

  keyWord = cgi['keyWord']
  [ 'Path', 'Descr', 'Comment', 'SharedLib', 'WantLib', 'Maintainer' ].each do |field|
    if cgi["search#{field}"] == "true" then
      resultsHash[field] = db.execute( selectHash[field], "%#{keyWord}%" )
    end
  end

  cgi.out_pretty {
    cgi.html {
      cgi.head {
        cgi.title { "Search Results" }
      } +
      cgi.body {
        cgi.openbsd_logo +
        cgi.h1 { "Search Result for <i>/#{keyWord}/</i>" } +
        cgi.hr +
        cgi.table( 'BORDER' => '2', 'CELLPADDING' => '4' ) {
          resultsHash.sort.inject("") {  |result,select|
            result + cgi.h2 { "#{ select[1].size } matches for #{ select[0] }" } +
            select[1].inject("") { |result,item|
              result + cgi.tr {
                cgi.td { cgi.a( cgi.package_href( item[0], showAll ) ) { item[0] } } +
                cgi.td { item[1] }
              }
            }
		  }
        } +
        mySignature(cgi)
      }
    }
  }

elsif mode == 'package' then

# put up a page summarizing a named package, with hyperlinking to referenced packages
# and links into the bsd.port.mk for column definitions

# edits provides detailed treatment of each possible row in the summary
# the default behavior is to use html entities for the characters & ' < >

  edits = Hash.new( proc { |value| if value != nil then CGI::escapeHTML(value) else "(nil)" end } )

  # hyperlink to referenced package
  edits['RUN_DEPENDS'] = proc { |value|
    if value != nil then
      value.split(' ').inject("") { |result,depend|
        a = depend.split(':')
        result + " #{a[0]}:#{a[1]}:" +
          cgi.a( cgi.package_href(a[2],showAll) ) { a[2] }
      }
    else
      value
    end
  }
  
  # hyperlink to referenced modules
  edits['MODULES'] = proc { |value|
    if value != nil then
      value.split(' ').inject("") { |result,depend|
        a = depend.split('/')
        if a.size == 2 then
          result + cgi.a( cgi.package_href(depend,showAll) ) { depend }
        else
          result + " #{depend}"
        end
      }
    else
      value
    end
  }
  
  edits['BUILD_DEPENDS'] = edits['RUN_DEPENDS']
  edits['REGRESS_DEPENDS'] = edits['RUN_DEPENDS']
  edits['LIB_DEPENDS'] = edits['RUN_DEPENDS']
  edits['HOMEPAGE'] = proc { |value| cgi.a( value ) { value } }

  # hyperlink from wantlibs to packages, note: muliple possible links due to flavors,
  # the javascript selector give a way to offer all possible links
  # the categories table identifies the system libs, e.g. libc, libm, libz, etc.
  edits['WANTLIB'] = proc { |value| 
    if value != nil then
      value.split(' ').inject("") do |result,libname|
        if libname == "" then
          result
        else
          count = db.get_first_value("select count(*) from system_libs where libname = ?", libname)
          if count == '0' then
            short_libname = libname.sub( /-(\.*\d+)*$/, "%" )
            librows = db.execute("select fullpkgpath from shared_libs where libname like ?", short_libname )
            if librows.length == 0 then
              result + " #{libname}"
            elsif librows.length == 1 then
              result + cgi.a( cgi.package_href(librows[0][0],showAll) ) { libname } + cgi.br
            else
              result + cgi.br + "\n" +
              cgi.form( 'NAME' => "#{libname}_form" ) {
                "#{libname}: " +
                cgi.select( 'NAME' => "#{libname}_select" ) {
                    librows.inject("") { |selector,packagePath|
                      selector + cgi.option(
                          'VALUE' => cgi.package_href(packagePath[0],showAll) ) { packagePath[0] }
                    }
                } +
                cgi.input( 'TYPE' => 'BUTTON', 'VALUE' => 'go', 'onClick' => "selectLink(this.form,'#{libname}_select')" )
              }
            end
          else
            result + " #{libname}" + cgi.br
          end
        end
      end 
    else
      value
    end
  }

  # columns is the column name metadata, rows is the result set of current values
  columns, rows = db.execute2( "select * from ports where fullpkgpath = ?", fullPkgPath )
  descr = db.get_first_value( "select value from descr where fullpkgpath = ?", fullPkgPath );

if rows == nil then

  cgi.out_pretty {
    cgi.html {
      cgi.body {
        cgi.h1 { fullPkgPath } +
        cgi.p {
          "No package with this name was found."
        }
      }
    } 
  }

else

  # rows_hash has same content as rows but is indexed by column names
  rows_hash = Hash.new
  (0 ... columns.length).to_a.inject(rows_hash) do |hash,index|
    hash[columns[index]]=rows[index]
    hash
  end
  columns = columns.sort

  cgi.out_pretty {

    cgi.html{
      cgi.head{
	    cgi.title{"Query Results"} +
        cgi.script( 'language' => 'JavaScript' ) {
<<EOT
<!-- js
function selectLink(form,name) {
  var index=form.elements[name].selectedIndex
  window.open(form.elements[name].options[index].value, target="_parent");
}
// end  -->
EOT
        }
      } +
      cgi.body{
        cgi.openbsd_logo +
        cgi.h1 { fullPkgPath } +
        cgi.p { descr } +
        cgi.table( "BORDER" => '2', "CELLPADDING" => '4' ){
          cgi.tr{
            cgi.th{ "Variable" } + cgi.th{ "Setting" }
          } +
          columns.inject("") { |result,colName|
            if rows_hash[colName] != nil || showAll then
              result +
              cgi.tr{
                cgi.td{
                  cgi.a("/bsd.port.mk.html\##{colName}"){colName}
                    # hyperlink to man page with variable's definition
                } +
                cgi.td{
                  cgi.b{cgi.tt{ edits[colName].call(rows_hash[colName]) }}
                }
              }
            else
              result
            end
          }
        } +
        mySignature(cgi)
      }
    }
  }
end

end
end
