#!/usr/bin/env ruby 

#################################################################
###### ######################################################## ##
##### ## Isam M. r0cketjump@yahoo.com   biodegradablegeek.com ## ##
#### ###   pre-alpha - Oct 09th, 2008   GPL License           ### ##
### ####                                                      #### ##
## ##### mksite.rb makes it easier/quicker to setup           ##### ##
### #### sites and web apps by doing the initial grunt        #### ##
#### ### work for you. This is alpha - 1. DO NOT USE!         ### ##
##### ##                                                      ## ##
###### ######################################################## ##
#################################################################


require 'rubygems'
require 'rio'
require 'mysql'
require 'highline/import'


### The following are required for DNS (optional) ### 
begin
  require 'apicore' 
  # Linode API key (login->My Profile) 
  API_Key = '' 
  # Server the domains will point to 
  ServerAddy = ''
  SOA_Email = ''
rescue LoadError
  puts "NOTICE: Unable to load apicore.rb - domains will not be added automatically"
end


Testing = false
Apache_sites = '/etc/apache/sites-available/'
Subdomains = ['', 'www', 'mail', "dev#{(rand*10).floor}"]
Homedir = ENV['HOME']
Username = ENV['USERNAME']
Applications = [:Skeleton, 'Clipshare (N/A)', 'Joomla (N/A)', 'PHPMotion (N/A)', 'PmWiki (N/A)', :Wordpress] 
$log = nil 


def say(txt)
  # super -> highline 
  super txt 
  $log << txt if $log
end

# flash('message', :notice or :error) to output msg
def flash(msg, message_type = :notice)
  if message_type.eql? :notice 
    say("INFORMATION: #{msg}") 
  elsif message_type.eql? :emphasize 
    say("***************************************************") 
    say("NOTICE: #{msg}\n") 
    say("***************************************************") 
  elsif message_type.eql? :error 
    STDERR.puts("FATAL ERROR: #{msg}\n") 
  end
end


if defined? ApiCore
  class DNSAPI < ApiCore 
    # This depends on the Linode API Ruby bindings (apicore.rb) 
    def initialize(key, debug=false, batching=false)
      super
      @batching=false
    end

    # Add domain. Return ID on success, nil on failure 
    def addDomain domain
      return nil if !domain
      params = {}
      params[:DomainID] = 0 
      params[:Domain] = domain 
      params[:Type] = 'master' 
      params[:Status] = 1 
      params[:SOA_Email] = SOA_Email 
      domainSave params 
    end

    def addDomainResource(domain, resource, target, record_type = 'A') 
      return nil if (did = getDomainIdByName domain).nil? 
      params = {} 
      params[:ResourceID] = 0 
      params[:DomainID] = did 
      params[:Name] = resource 
      params[:Type] = record_type 
      params[:Target] = target 
      domainResourceSave params 
    end

    def getDomainIdByName domain
      domainList.find do |dom| 
        return dom["DOMAINID"] if dom["DOMAIN"].downcase == domain.downcase
      end
    end
  end
else
  flash('ApiCore not loaded. Skipping DNS stuff', :notice)
end


class App
  class << self; attr_reader :message, :name, :version, :description, :vhost; end
  @message = nil
  @name = nil
  @version = nil
  @description = 'Just Another Web App'
  @vhost = 'generic'

  def initialize(rootdir, domain, db)
    flash("Initializing application (dir=#{rootdir}, domain=#{domain})")
    @rootdir = (rootdir[-1].chr.eql? '/') ? rootdir.chop : rootdir
    @domain = domain
    @db = db # hash of database info 

    # System stuff 
    if Testing
      @templates = '/home/kiwi/Code/mksite/templates'
      @configs = '/home/kiwi/Code/mksite/configs'
      @vhosts = '/home/kiwi/Code/mksite/vhosts'
    else
      @templates = '/etc/mksite/templates'
      @configs = '/etc/mksite/configs'
      @vhosts = '/etc/mksite/vhosts'
    end
  end

  # This generally does not need to be overriden. 
  # It does 'generic shit' like creating the rootdir 
  # and setting permissions 
  def envSetup
    if !rio(@rootdir).exist? 
      flash("Creating directory #{@rootdir}")
      rio(@rootdir).mkpath
    end

    rio(@rootdir).chdir do |root|
      # Copy the generic public/private/log apache 
      # structure to rootdir 
      flash("Changing working dir to #{@rootdir}")
      flash("Working inside '#{root.to_s}'")
      rio(@templates,'skeleton.www').each { |df| df > root } 

      # Set permissions (a+w on logs, etc) 
      flash('Setting permissions...') 
      flash('666 ./log/*.log')
      rio('./log/access.log').chmod(00666)
      rio('./log/error.log').chmod(00666)
      flash('700 ./private')
      rio('./private/').chmod(00700)
    end
  end

  def setup
    # envSetup() 
    # databaseSetup()
    raise 'OVERRIDE ME'
  end

  def databaseSetup
    # Create database if it doesn't already exist 
    # This usually doesn't need to be overriden 
    #Mysql.server_connect(@db['name'])
    #  flash('Checking database connection... ') 
    begin
      dbo = Mysql.real_connect(@db['host'], @db['user'], @db['pass']) 
      flash("Creating database '#{@db['name']}'"); 
      res = dbo.query("CREATE DATABASE IF NOT EXISTS #{@db['name']};") 
      flash("Database server returned #{res}") if res 
    rescue Mysql::Error => err
      flash('Unable to connect to access/create database', :error)
      flash("Error returned (#{err.errno}) = '#{err.error}'", :error)
      exit 1
    ensure
      dbo.close if dbo 
    end
  end

  def dnsSetup 
    return if !defined? ApiCore

    # Setup DNS - currently uses Linode API 
    flash("Setting up DNS for '#{@domain}'") 
    flash("This requires a Linode API key\nLogin to linode.com and find it under 'My Profile'", :emphasize) 

    dns = api_key = nil
    loop do
      api_key = Testing ? API_Key : ask('Paste your Linode API key (or \'skip\'): ') 
      break if api_key.downcase.eql? 'skip'

      # Check API key 
      begin
        dns = DNSAPI.new api_key
        dns.domainList
      rescue RuntimeError
        flash("API key invalid (or API service is down). Skipping DNS stuff") 
        api_key = nil
      end
      break 
    end

    unless api_key.nil? or api_key.downcase.eql? 'skip'
      flash("Adding master domain '#{@domain}'") 
      begin
        dns.addDomain(@domain) 
      rescue RuntimeError
        flash('Unable to add domain (exists already?). Attempting to add subdomains...', :error) 
      end

      Subdomains.each do |sub|
        flash("Adding subdomain '#{sub}.#{@domain}'") 
        flash('Could not add subdomain', :error) if !dns.addDomainResource(@domain, sub, ServerAddy) 
      end
    end
  end

  def serverSetup 
    # Set Apache vhost 
    vhost_file = (defined? self.class.vhost) ? self.class.vhost : 'generic'
    flash("Generating vhost file (#{@vhosts}/#{vhost_file}) for Apache 2.x") 
    vhost = '' 
    rio(@vhosts, vhost_file) > vhost 
    vhost.gsub!('_MKS_DOMAIN_', @domain) 
    vhost.gsub!('_MKS_ROOT_', "#{@rootdir}") 
    vhost.gsub!('_MKS_PUBLIC_', "#{@rootdir}/public") 
    rio(Testing ? '/tmp/' : Apache_sites, @domain).puts(vhost) 
  end
end


class Skeleton < App
  @name = 'Skeleton'
  @description = 'Generic WWW directory structure'
  @message = '' 
  @vhost = 'generic'

  def setup
    self.envSetup() 
    self.dnsSetup() 
    self.serverSetup() 
  end
end

class Wordpress < App
  @name = 'Wordpress'
  @version = '2.6.2'
  @description = 'A popular blogging platform'
  @message = 'Trying to make monies on the Internets?' 
  @vhost = 'generic'

  def setup
    # This sets up the initial environment / permissions 
    self.envSetup() 

    # Copy the wordpress skeleton directory to the new dir 
    flash('Copying Wordpress data over...') 
    wproot = rio(@rootdir,'public') 
    flash("Wordpress root will be #{wproot}") 
    rio(@templates,'wordpress').each { |df| df > wproot } 
    rio(wproot).chdir do
      rio('wp-config-sample.php').rm()
      # Generate and output wp config 
      flash('Generating wp-config.php based on your DB settings...') 
      wpcfg = rio(@configs, 'wordpress.cfg') 
      if !wpcfg.exist? or !wpcfg.readable? 
        flash("Wordpress config template missing or unreadable, quitting", :error)
        exit 2
      end

      # Copy the config into a string, do things with it and then write it to disk 
      config = ''
      rio(@configs, 'wordpress.cfg') > config 
      flash("Warning: config file '#{@configs}/wordpress.cfg' is empty") if config.empty? 
      config['_MKS_DB_HOST'] = @db['host']
      config['_MKS_DB_USER'] = @db['user']
      config['_MKS_DB_PASS'] = @db['pass']
      config['_MKS_DB_NAME'] = @db['name']
      rio('wp-config.php').w!.puts(config) 
    end

    # Setup the database 
    self.databaseSetup() 

    # Add DNS info 
    self.dnsSetup() 

    # Setup the server/vhost 
    self.serverSetup() 
  end
end



class Log
  def initialize()
    # Need a puts() else the tmp object will only 
    # exist in memory until the first write/read 
    @log = rio(??, basename='mksite').puts(Time.now.to_i)
    flash("Log file generated (important): #{@log.to_s}", :emphasize)
  end

  # append to log 
  def <<(data)
    @log << data << "\n"
  end
end


def main
  flash('Press ^C (CTRL+C) to end this at any time', :emphasize)
  $log = Log.new

  # Let's ask neutral questions about the new site. 
  if Testing
    rootdir = '/tmp/sandbox9/'
    domain = 'domain.cxm'
  else
    domain = ask('Enter site\'s domain name (no http:// or www): ') #{ |d| d.validate = !! /^www\.|^http:\/\// }
    rootdir = ask('Root site directory (leave blank for default): ') { |q| 
      q.default = "/home/#{Username}/www/#{domain}/"
      q.validate = /^\/home\/#{Username}\// 
    } 
  end

  flash("Domain has been set to \"#{domain}\"")
  flash("Site will reside in \"#{rootdir}\", and the index/script")
  flash(" files (index.php, .htaccess etc) will go in \"#{rootdir}/public/\"\n\n")

  # Ask the user what app she wants to install and specific questions about that app 
  app = nil
  choose do |menu|
    menu.prompt = 'Choose the software for your new site: '
    Applications.each do |app|
      menu.choice app do |a|
        app = a
      end
    end
  end

  flash("You chose #{app}. #{Kernel.const_get(app).message}\n") 

  # Fetch DB info if this app needs a database 
  # XXX Should just have a db flag in the App's class 
  db = nil
  unless app.eql? :Skeleton 
    flash('This app requires a database (only MySQL supported)')
    flash('It will be created if it doesn\'t exist')

    db = {} 
    db['host'] = 'localhost'
    loop do
      if Testing
        db['user'] = 'kiwi'
        db['pass'] = ''
        db['name'] = 'kiwi_sandbox9'
      else
        db['user'] = ask('Enter MySQL username: ') 
        db['pass'] = ask('Enter MySQL password: ') { |q| q.echo = '*'} 
        db['name'] = ask('Enter database name (should begin w/ your \'username_\')') { |dbn| dbn.validate = /^#{Username}_/}
      end
      flash("Please double check: mysql://#{db['user']}:#{db['pass']}@#{db['host']}/#{db['name']}")
      break if Testing or agree('Is this correct?') 
      flash("Please re-enter database info\n")
    end
  end

  # Check permissions and database login 
  flash('Doing a preliminary check', :emphasize)
  flash('Checking installation environment') 
  # Quit if the directory exists and the user does not want to go ahead 
  unless Testing
    exit 1 if rio(rootdir).exist? && !agree("Directory '#{rootdir}' exists! Continue? (Y/N)") 
  end

  # Is the parent directory available and writable? 
  parentdir = rio(rootdir).dirname
  unless parentdir.exist? && parentdir.writable? 
    flash("Directory '#{parentdir.to_s}' either doesn't exist or is not writable. Check permissions", :error)
    exit 1
  end

  unless db.nil? 
    # Check DB connection - but this does not check if user has any privileges 
    flash('Checking database connection') 
    begin
      dbo = Mysql.real_connect(db['host'], db['user'], db['pass']) 
      flash("Successfully connected to '#{dbo.get_server_info}'") 
    rescue Mysql::Error => err
      flash('Unable to connect to database server', :error)
      flash("Error returned (#{err.errno}) = '#{err.error}'", :error)
      exit 1
    ensure
      dbo.close if dbo 
    end
  end

  # Begin installation 
  flash("Word on the server racks is... you\'re good to go", :emphasize) 
  Kernel.const_get(app).new(rootdir, domain, db).setup() 
end

main() 



