#!/usr/bin/env ruby
# Utility function to scan various scripts
#   Docs: for Wvisible PAGETITLE and categories in .cgi
#   Repos: for ASF::SVN access in .cgi|rb
$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'wunderbar'
require 'whimsy/asf'
SCANDIR = '../www'
ISERR = '!'
AUTHMAP = { # From whimsy-vm4.apache.org.yaml
  'ASF Committers' => 'text-muted',
  'ASF Members and Officers' => 'text-primary',
  'ASF Members and Incubator PMC' => 'text-success',
  'ASF Members' => 'text-warning',
  'ASF Secretarial Team' => 'text-danger'
}
AUTHPUBLIC = 'glyphicon-eye-open'
ASFSVN = /ASF::SVN/
SCANDIRSVN = '../'
WWWAUTH = /WWW-Authenticate: Basic realm/
CONSTANT_DEF = /(?<matchconst>[A-Z_]+)\s+=\s+['"](?<matchval>[^#]+)['"]/ # Attempt to capture CONSTANT = "value"

HTTPD_SITES = '/etc/apache2/sites-enabled'
# Use wild-card to allow for possible renames (normally 10-whimsy-vm-443.conf)
# Also allows testing on a developer system (use a different suffix that is not Included by httpd)
WHIMSY_CONF = File.join(HTTPD_SITES, '*-whimsy-vm-443.*')

# Output ul of key of AUTHMAP for use in helpblock
def emit_authmap
  _ul do
    _li do
      _span.glyphicon :aria_hidden, :class => AUTHPUBLIC
      _ 'Publicly available'
    end
    AUTHMAP.each do |realm, style|
      _li do
        _span.glyphicon.glyphicon_lock :aria_hidden, :class =>  style, aria_label: realm
        _ realm
      end
    end
  end
end

# Output a span with the auth level
def emit_auth_level(level)
  if level
    _span :class =>  level, aria_label: AUTHMAP.key(level) do
      _span.glyphicon.glyphicon_lock :aria_hidden
    end
  else
    _span.glyphicon :aria_hidden, :class =>  AUTHPUBLIC
  end
end

# Scan single file for PAGETITLE and categories when Wvisible
# @return [PAGETITLE, [cat,egories] ] or ["!Bogosity error", "stacktrace"]
def scan_file(f)
  begin
    File.open(f).each_line.map(&:chomp).each do |line|
      if line =~ /\APAGETITLE\s?=\s?"([^"]+)"\s?#\s?Wvisible:(.*)/i then
        return [$1, $2.chomp.split(%r{[\s,]})]
      end
    end
    return nil
  rescue Exception => e
    return ["#{ISERR}Bogosity! #{e.message[0..255]}", "\t#{e.backtrace.join("\n\t")}"]
  end
end

# Return data only about Wvisible cgis, plus any errors
# @return [ [PAGETITLE, [cat,egories] ], ... ]
def scan_dir(dir)
  links = {}
  Dir["#{dir}/**/*.cgi"].each do |f|
    l = scan_file(f)
    links[f.sub(dir, '')] = l if l
  end
  return links
end


# Parse httpd config file so we can annotate links with access hints
# Sample data:
# <LocationMatch ^/board/subscriptions>
#   AuthName "ASF Committers"
# <Directory /x1/srv/whimsy/www/committers>
#   AuthName "ASF Committers"
# @return { "/path" => "auth realm",... }
def get_auth
  hash = {}
  files = Dir[WHIMSY_CONF]
  return hash unless files.size == 1 # must match just one
  file = files.first
  loc = nil
  File.read(file).each_line do |l|
    if l =~ %r{<LocationMatch ([^>]+)>}
      loc = $1.gsub(/^\^/,'') # remove ^ prefix
    elsif l =~ %r{<Directory ([^>]+)>}
      # remove standard prefix and append '/' directory marker
      loc = $1.sub(%r{^(/x1)?/srv/whimsy/www},'')+'/'
    elsif l =~ %r{AuthName\s+"(.+)"} # generate the entry
      hash[loc] = $1 if loc
      loc = nil
    end
  end
  hash
end

# Annotate scan_dir entries with hints only for paths that require auth
# Side Effects:
#   - REMOVES any error scan entries
#   - Adds array element of auth realm if login required
def annotate_scan(scan, auth)
  annotated = scan.reject{ |_k, v| v[0] =~ /\A#{ISERR}/ }
  annotated.each do |path, ary|
    realm = auth.select { |k, _v| path.match(/\A#{k}/) }
    if realm.values.first
      ary << AUTHMAP[realm.values.first]
    end
  end
  return annotated
end

# Common use case # TODO these could be static generated files nightly
def get_annotated_scan(dir)
  scan = scan_dir(dir)
  auth = get_auth()
  return annotate_scan(scan, auth)
end

# Build a regex union from ASFSVN and an array
# @return Regexp.union(r...)
def build_regexp(list)
  r = []
  list.each do |itm|
    r << "#{ASFSVN.source}\['#{itm}']"
  end
  return Regexp.union(r)
end

# Scan file for use of ASF::SVN symbolic names like apmail_bin; unmapping any CONSTANT_DEF
# @return [["x = ASF::SVN['Meetings'] # Whole line of code accessing private repo", ...], [<public repos same>], 'WWW-Authenticate code line' ]
def scan_file_svn(f, regexs)
  repos = [[], [], []]
  consts = {}
  begin
    File.open(f).each_line.map(&:chomp).each do |line|
      line.strip!
      if line =~ WWWAUTH # Fastest compare first
        repos[2] << line
      elsif line =~ ASFSVN # Find all ASF::SVN and also map if it uses a CONSTANT_DEF
        consts.each do |k,v|
          line.sub!(k, v)
        end
        if line =~ regexs[0]
          repos[0] << line
        elsif line =~ regexs[1]
          repos[1] << line
        end
      elsif line =~ CONSTANT_DEF
        consts[$~['matchconst']] = "'#{$~['matchval']}'"
      end
    end
    return repos
  rescue Exception => e
    return [["#{ISERR}Bogosity! #{e.message[0..255]}", "\t#{e.backtrace.join("\n\t")}"],[]]
  end
end

# Scan directory for use of ASF::SVN (private or public)
# @return { "file" => [['private line', ...], ['public svn', ...], 'WWW-Authenticate code line' (, 'authrealm')] }
def scan_dir_svn(dir, regexs)
  links = {}
  auth = get_auth()
  Dir["#{dir}/**/*.{cgi,rb}"].sort.each do |f|
    l = scan_file_svn(f, regexs)
    if (l[0].length + l[1].length) > 0
      fbase = f.sub(dir, '')
      realm = auth.select { |k, _v| fbase.sub('/www', '').match(/\A#{k}/) }
      if realm.values.first
        l << AUTHMAP[realm.values.first]
      end
      links[fbase] = l
    end
  end
  return links
end
