#!/usr/bin/ruby
#  Phusion Passenger - https://www.phusionpassenger.com/
#  Copyright (c) 2010-2013 Phusion
#
#  "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#  THE SOFTWARE.

## Magic comment: begin bootstrap ##
source_root = File.expand_path("..", File.dirname(__FILE__))
$LOAD_PATH.unshift("#{source_root}/lib")
begin
	require 'rubygems'
rescue LoadError
end
require 'phusion_passenger'
## Magic comment: end bootstrap ##

PhusionPassenger.locate_directories

# The Apache executable may be located in an 'sbin' folder. We add
# the 'sbin' folders to $PATH just in case. On some systems
# 'sbin' isn't in $PATH unless the user is logged in as root from
# the start (i.e. not via 'su' or 'sudo').
ENV["PATH"] += ":/usr/sbin:/sbin:/usr/local/sbin"

require 'optparse'
require 'stringio'
PhusionPassenger.require_passenger_lib 'constants'
PhusionPassenger.require_passenger_lib 'platform_info/ruby'
PhusionPassenger.require_passenger_lib 'platform_info/apache'
PhusionPassenger.require_passenger_lib 'platform_info/apache_detector'
PhusionPassenger.require_passenger_lib 'abstract_installer'
PhusionPassenger.require_passenger_lib 'utils/terminal_choice_menu'

class Installer < PhusionPassenger::AbstractInstaller
	include PhusionPassenger
	TerminalChoiceMenu = PhusionPassenger::Utils::TerminalChoiceMenu
	
	def dependencies
		specs = [
			'depcheck_specs/compiler_toolchain',
			'depcheck_specs/ruby',
			'depcheck_specs/gems',
			'depcheck_specs/libs',
			'depcheck_specs/apache2'
		]
		ids = [
			'cc',
			'c++',
			'libcurl-dev',
			'openssl-dev',
			'zlib-dev',
			'apache2',
			'apache2-dev',
			'rake',
			'ruby-openssl',
			'rubygems'
		]
		if @languages.include?("ruby")
			if PlatformInfo.passenger_needs_ruby_dev_header?
				ids << 'ruby-dev'
			end
			ids << 'rack'
		end
		# Some broken servers don't have apr-config or apu-config installed.
		# Nevertheless, it is possible to compile Apache modules if Apache
		# was configured with --included-apr. So here we check whether
		# apr-config and apu-config are available. If they're not available,
		# then we only register them as required dependency if no Apache
		# module can be compiled without their presence.
		if (PlatformInfo.apr_config && PlatformInfo.apu_config) ||
		   PlatformInfo.apr_config_needed_for_building_apache_modules?
			ids << 'apr-dev'
			ids << 'apu-dev'
		end
		return [specs, ids]
	end
	
	def users_guide_path
		return PhusionPassenger.apache2_doc_path
	end

	def users_guide_url
		return APACHE2_DOC_URL
	end
	
	def run_steps
		if PhusionPassenger.natively_packaged?
			if apache_module_available?
				notify_apache_module_installed
				show_deployment_example
			else
				install_apache_module_from_native_package || exit(1)
			end
			exit
		end
		
		Dir.chdir(PhusionPassenger.source_root)
		show_welcome_screen
		query_interested_languages
		check_gem_install_permission_problems || exit(1)
		check_directory_accessible_by_web_server
		check_dependencies || exit(1)
		check_whether_there_are_multiple_apache_installs || exit
		check_whether_apache_uses_compatible_mpm
		check_whether_os_is_broken
		check_whether_system_has_enough_ram
		check_write_permission_to_passenger_root || exit(1)
		if install_apache2_module
			show_apache2_config_snippets
			show_deployment_example
		else
			show_possible_solutions_for_compilation_and_installation_problems
			exit(1)
		end
	end

private
	def show_welcome_screen
		render_template 'apache2/welcome', :version => VERSION_STRING
		wait
	end

	def query_interested_languages
		menu = TerminalChoiceMenu.new(["Ruby", "Python", "Node.js", "Meteor"])
		menu["Ruby"].checked = interesting_language?('ruby')
		menu["Python"].checked = interesting_language?('python')
		menu["Node.js"].checked = interesting_language?('nodejs', 'node')
		menu["Meteor"].checked = interesting_language?('meteor')

		new_screen
		puts "<banner>Which languages are you interested in?</banner>"
		puts
		if interactive?
			puts "Use <space> to select."
		else
			puts "Override selection with --languages."
		end
		puts

		if interactive?
			begin
				menu.query
			rescue Interrupt
				raise Abort
			end
		else
			menu.display_choices
			puts
		end

		@languages = menu.selected_choices.map{ |x| x.downcase.gsub(/\./, '') }
	end

	def interesting_language?(name, command = nil)
		if @languages
			return @languages.include?(name)
		else
			return !!PlatformInfo.find_command(command || name)
		end
	end

	def check_whether_there_are_multiple_apache_installs
		new_screen
		puts '<banner>Sanity checking Apache installation...</banner>'

		output = StringIO.new
		detector = PlatformInfo::ApacheDetector.new(output)
		begin
			detector.detect_all
			detector.report
			@apache2 = detector.result_for(PlatformInfo.apxs2)
			if @apache2.nil?
				# Yes this can happen (see https://groups.google.com/forum/#!topic/phusion-passenger/JcUJOBzILB4)
				# but I have no idea how and why. I need people who can help me reproduce this.
				# Until then, let's output an error.
				raise "An internal error occurred. No information detected for #{PlatformInfo.apxs2} " +
					"(@apache2 is nil). Please contact this program's authors for support, and please " +
					"attach the full output of this installer, as well as the full output of the command " +
					"'passenger-config --detect-apache2'."
			end
			if detector.results.size > 1
				other_installs = detector.results - [@apache2]
				render_template 'apache2/multiple_apache_installations_detected',
					:current => @apache2,
					:other_installs => other_installs
				puts
				if interactive?
					result = prompt_confirmation "Are you sure you want to install " +
						"against Apache #{@apache2.version} (#{@apache2.apxs2})?"
					if !result
						puts
						line
						render_template 'apache2/installing_against_a_different_apache',
							:other_installs => other_installs
					end
					return result
				else
					puts '<yellow>Continuing installation because --auto is given.</yellow>'
				end
			else
				return true
			end
		ensure
			detector.finish
		end
	end
	
	def check_whether_apache_uses_compatible_mpm
		# 'httpd -V' output is in the form of:
		#
		# Server MPM:      Prefork     # <--- this line is not always available!
		# ...
		# Server compiled with....
		#  -D APACHE_MPM_DIR="server/mpm/prefork"
		output = `#{PlatformInfo.httpd} -V`
		output =~ /^Server MPM: +(.*)$/
		if $1
			mpm = $1.downcase
		else
			output =~ /APACHE_MPM_DIR="server\/mpm\/(.*)"/
			if $1
				mpm = $1.downcase
			else
				mpm = nil
			end
		end
		if mpm != "prefork" && mpm != "worker" && mpm != "event"
			new_screen
			render_template 'apache2/apache_must_be_compiled_with_compatible_mpm',
				:current_mpm => mpm
			wait
		end
	end
	
	def check_write_permission_to_passenger_root
		File.new("__test__.txt", "w").close
		return true
	rescue SystemCallError
		puts
		line
		if Process.uid == 0
			render_template 'apache2/no_write_permission_to_passenger_root'
		else
			render_template 'installer_common/run_installer_as_root',
				:dir  => PhusionPassenger.source_root,
				:sudo => PhusionPassenger::PlatformInfo.ruby_sudo_command,
				:sudo_s_e => PhusionPassenger::PlatformInfo.ruby_sudo_shell_command("-E"),
				:ruby => PhusionPassenger::PlatformInfo.ruby_command,
				:installer => "#{PhusionPassenger.bin_dir}/passenger-install-apache2-module #{ORIG_ARGV.join(' ')}"
		end
		return false
	ensure
		File.unlink("__test__.txt") rescue nil
	end
	
	def install_apache2_module
		puts
		line
		puts '<banner>Compiling and installing Apache 2 module...</banner>'
		puts "cd #{PhusionPassenger.source_root}"
		if ENV['TRACE']
			puts "#{PlatformInfo.rake_command} --trace apache2:clean apache2 RELEASE=yes"
			return sh("#{PlatformInfo.rake_command} --trace apache2:clean apache2 RELEASE=yes")
		else
			puts "#{PlatformInfo.rake_command} apache2:clean apache2 RELEASE=yes"
			return sh("#{PlatformInfo.rake_command} apache2:clean apache2 RELEASE=yes")
		end
	end
	
	def show_apache2_config_snippets(bare = false)
		if bare
			puts "LoadModule passenger_module #{PhusionPassenger.apache2_module_path}"
			puts "PassengerRoot #{PhusionPassenger.source_root}"
			puts "PassengerDefaultRuby #{PlatformInfo.ruby_command}"
		else
			puts
			line
			render_template 'apache2/config_snippets',
				:module_location => PhusionPassenger.apache2_module_path,
				:passenger_root => PhusionPassenger.source_root,
				:ruby => PlatformInfo.ruby_command
			if PhusionPassenger.originally_packaged?
				wait
			else
				wait(10)
			end
		end
	end
	
	def show_deployment_example
		new_screen
		render_template 'apache2/deployment_example',
			:users_guide_path => users_guide_path,
			:users_guide_url => users_guide_url,
			:phusion_website => PHUSION_WEBSITE,
			:passenger_website => PASSENGER_WEBSITE,
			:languages => @languages
	end
	
	def show_possible_solutions_for_compilation_and_installation_problems
		new_screen
		render_template 'apache2/possible_solutions_for_compilation_and_installation_problems',
			:users_guide_path => users_guide_path,
			:users_guide_url => users_guide_url,
			:support_url => SUPPORT_URL
	end

	def apache_module_available?
		return File.exist?(PhusionPassenger.apache2_module_path)
	end

	def install_apache_module_from_native_package
		case PhusionPassenger.native_packaging_method
		when 'deb'
			sh! "sudo apt-get update"
			sh! "sudo apt-get install #{DEB_APACHE_MODULE_PACKAGE}"
			return true
		when 'rpm'
			sh! "sudo yum install #{RPM_APACHE_MODULE_PACKAGE}-#{VERSION_STRING}"
			return true
		else
			puts "<red>The #{PROGRAM_NAME} Apache module package is not installed.</red>"
			puts "Please ask your operating system vendor how to install it."
			return false
		end
	end

	def notify_apache_module_installed
		render_template 'apache2/notify_apache_module_installed'
		wait
	end
end

ORIG_ARGV = ARGV.dup
options = {}
parser = OptionParser.new do |opts|
	opts.banner = "Usage: passenger-install-apache2-module [options]"
	opts.separator ""
	
	indent = ' ' * 37
	opts.separator "Options:"
	opts.on("-a", "--auto", String, "Automatically build the Apache module,\n" <<
	        "#{indent}without interactively asking for user\n" <<
	        "#{indent}input.") do
		options[:auto] = true
	end
	opts.on("--apxs2-path PATH", String, "Path to 'apxs2' command.") do |value|
		ENV['APXS2'] = value
	end
	opts.on("--apr-config-path PATH", String, "Path to 'apr-config' command.") do |value|
		ENV['APR_CONFIG'] = value
	end
	opts.on("--languages NAMES", "Comma-separated list of interested\n" <<
			"#{indent}languages (e.g.\n" <<
			"#{indent}'ruby,python,nodejs,meteor')") do |value|
		options[:languages] = value.split(",")
	end
	opts.on("--snippet", "Show just the Apache config snippet.") do
		options[:snippet] = true
	end
end
begin
	parser.parse!
rescue OptionParser::ParseError => e
	puts e
	puts
	puts "Please see '--help' for valid options."
	exit 1
end

installer = Installer.new(options)
if options[:snippet]
	installer.send(:show_apache2_config_snippets, true)
else
	installer.run
end
