# Copyright (c) 2008, Jannis Leidel
# All rights reserved.
#
# Changelog
# 
# 0.1   - initial release
# 0.1.1 - some fixes with dependencies for virtualmin-svn
# 0.1.2 - fixes for postgres installation errors
# 0.2   - now uses <Location> instead of ScriptAlias directives for
#         compatibility reasons (a.k.a don't destroy other install scripts)

@trac_tables = ('attachment', 'auth_cookie', 'cache', 'component', 'enum', 'milestone', 'node_change', 'notify_subscription', 'notify_watch', 'permission', 'report', 'repository', 'revision', 'session', 'session_attribute', 'system', 'ticket', 'ticket_change', 'ticket_custom', 'version', 'wiki');

# script_trac_desc()
sub script_trac_desc
{
return "Trac";
}

sub script_trac_uses
{
return ( "python", "apache" );
}

sub script_trac_longdesc
{
return "Enhanced wiki and issue tracking system for software development projects";
}

sub script_trac_author
{
return "Jannis Leidel";
}

# script_trac_versions()
sub script_trac_versions
{
return ( "1.4.3", "1.2.6", "1.0.20" );
}

sub script_trac_release
{
return 2;	# Fixed UTF-8 issue
}

sub script_trac_version_desc
{
local ($ver) = @_;
return &compare_versions($ver, 1.2) < 0 ? "$ver (Old stable)" :
       &compare_versions($ver, 1.4) < 0 ? "$ver (New stable)" :
					  "$ver (Latest stable)";
}

sub script_trac_category
{
return "Tracker";
}

sub script_trac_python_modules
{
local ($d, $ver, $opts) = @_;
local ($dbtype, $dbname) = split(/_/, $opts->{'db'}, 2);
return ( "setuptools", "svn", $dbtype eq "mysql" ? "MySQLdb" : "psycopg",
	 "genshi" );
}

# script_trac_depends(&domain, version)
# Check for python, svn and SVN plugin
sub script_trac_depends
{
local ($d, $ver) = @_;
my $python = &has_command($config{'python_cmd'} || "python");
$python || push(@rv, "The python command is not installed");
&has_command("svn") || return "The svn command is not installed";
&require_apache();
$d->{'virtualmin-svn'} || return "The SVN plugin is not enabled for this domain";
local $conf = &apache::get_config();
local $got_rewrite;
foreach my $l (&apache::find_directive("LoadModule", $conf)) {
	$got_rewrite++ if ($l =~ /mod_rewrite/);
	}
$apache::httpd_modules{'mod_fcgid'} ||
	return "Apache does not have the mod_fcgid module";
$apache::httpd_modules{'mod_rewrite'} || $got_rewrite ||
	return "Apache does not have the mod_rewrite module";

# Check if any SVN repos exist
&foreign_require("virtualmin-svn");
local @reps = &virtualmin_svn::list_reps($d);
local @users = &virtualmin_svn::list_users($d);
scalar(@reps) && scalar(@users) ||
	return "At least one SVN repository and a user with access to it ".
	       "must exist in the domain";
return undef;
}

# script_trac_params(&domain, version, &upgrade-info)
# Returns HTML for table rows for options for installing PHP-NUKE
sub script_trac_params
{
local ($d, $ver, $upgrade) = @_;
local $rv;
local $hdir = &public_html_dir($d, 1);
if ($upgrade) {
	# Options are fixed when upgrading
	local ($dbtype, $dbname) = split(/_/, $upgrade->{'opts'}->{'db'}, 2);
	$rv .= &ui_table_row("Database for Trac", $dbname);
	$rv .= &ui_table_row("SVN repository", $upgrade->{'opts'}->{'rep'});
	$rv .= &ui_table_row("Trac project name", $upgrade->{'opts'}->{'project'});
	local $dir = $upgrade->{'opts'}->{'dir'};
	$dir =~ s/^$d->{'home'}\///;
	$rv .= &ui_table_row("Install directory", $dir);
	$rv .= &ui_table_row("Trac admin user", $upgrade->{'opts'}->{'tracadmin'});
	}
else {
	# Show editable install options
	&foreign_require("virtualmin-svn", "virtualmin-svn-lib.pl");
	local @dbs = &domain_databases($d, [ "mysql", "postgres" ]);
	local @reps = &virtualmin_svn::list_reps($d);
	local @users = &virtualmin_svn::list_users($d);
	$rv .= &ui_table_row("Database for Trac",
			 &ui_database_select("db", undef, \@dbs, $d, "trac"));
	$rv .= &ui_table_row("SVN repository",
			 &ui_select("rep", undef,
			 [ map { [ $_->{'rep'}, $_->{'rep'} ] } @reps ]));
	$rv .= &ui_table_row("Trac project name",
			 &ui_textbox("project", "trac", 30));
	$rv .= &ui_table_row("Install sub-directory under <tt>$hdir</tt>",
				 &ui_opt_textbox("dir", undef, 30,
						 "At top level"));
	$rv .= &ui_table_row("Trac admin user",
			 &ui_select("tracadmin", undef,
			 [ map { [ $_->{'user'}, $_->{'user'} ] } @users ]));
	}
return $rv;
}

# script_trac_parse(&domain, version, &in, &upgrade-info)
# Returns either a hash ref of parsed options, or an error string
sub script_trac_parse
{
local ($d, $ver, $in, $upgrade) = @_;
if ($upgrade) {
	# Options are always the same
	return $upgrade->{'opts'};
	}
else {
	local $hdir = &public_html_dir($d, 0);
	$in->{'dir_def'} || $in->{'dir'} =~ /\S/ && $in->{'dir'} !~ /\.\./ ||
		return "Missing or invalid installation directory";
	local $dir = $in->{'dir_def'} ? $hdir : "$hdir/$in->{'dir'}";
	$in{'project'} =~ /^[a-z0-9]+$/ ||
		return "Project name can only contain letters and numbers";
	local ($newdb) = ($in->{'db'} =~ s/^\*//);
	return { 'db' => $in->{'db'},
		 'newdb' => $newdb,
		 'dir' => $dir,
		 'rep' => $in{'rep'},
		 'tracadmin' => $in{'tracadmin'},
		 'path' => $in->{'dir_def'} ? "/" : "/$in->{'dir'}",
		 'project' => $in{'project'} };
	}
}

# script_trac_check(&domain, version, &opts, &upgrade-info)
# Returns an error message if a required option is missing or invalid
sub script_trac_check
{
local ($d, $ver, $opts, $upgrade) = @_;
$opts->{'dir'} =~ /^\// || return "Missing or invalid install directory";
$opts->{'db'} || return "Missing database";
$opts->{'rep'} || return "Missing SVN repository name";
if (-r "$opts->{'dir'}/trac.fcgi") {
	return "Trac appears to be already installed in the selected directory";
	}
$opts->{'project'} || return "Missing Trac project name";
$opts->{'tracadmin'} || return "Missing Trac admin user";
$opts->{'project'} =~ /^[a-z0-9]+$/ ||
	return "Project name can only contain letters and numbers";
return undef;
}

# script_trac_files(&domain, version, &opts, &upgrade-info)
# Returns a list of files needed by Rails, each of which is a hash ref
# containing a name, filename and URL
sub script_trac_files
{
local ($d, $ver, $opts, $upgrade) = @_;
local @files = (
	 { 'name' => "source",
	   'file' => "Trac-$ver.tar.gz",
	   'url' => "http://ftp.edgewall.com/pub/trac/Trac-$ver.tar.gz" },
	 { 'name' => "flup",
	   'file' => "flup-1.0.tar.gz",
	   'url' => "http://www.saddi.com/software/flup/dist/flup-1.0.tar.gz" },
	);
return @files;
}

sub script_trac_commands
{
local ($d, $ver, $opts) = @_;
return ($config{'python_cmd'} || "python");
}

# script_trac_install(&domain, version, &opts, &files, &upgrade-info)
# Actually installs PhpWiki, and returns either 1 and an informational
# message, or 0 and an error
sub script_trac_install
{
local ($d, $version, $opts, $files, $upgrade) = @_;
local ($out, $ex);

# Get database settings
local ($dbtype, $dbname) = split(/_/, $opts->{'db'}, 2);
if ($opts->{'newdb'} && !$upgrade) {
	local $dbopts;
	if ($dbtype eq "mysql" && &compare_versions($version, "0.12.3") >= 0) {
		$dbopts = { 'charset' => 'utf8',
			    'collate' => 'utf8_bin' };
		}
	local $err = &create_script_database($d, $opts->{'db'}, $dbopts);
	return (0, "Database creation failed : $err") if ($err);
	}
local $dbuser = $dbtype eq "mysql" ? &mysql_user($d) : &postgres_user($d);
local $dbpass = $dbtype eq "mysql" ? &mysql_pass($d) : &postgres_pass($d, 1);
local $dbhost = &get_database_host($dbtype, $d);
if ($dbtype) {
	local $dberr = &check_script_db_connection($dbtype, $dbname,
						   $dbuser, $dbpass);
	return (0, "Database connection failed : $dberr") if ($dberr);
	}
my $python = &has_command($config{'python_cmd'} || "python");
$python || push(@rv, "The python command is not installed");

# Create target dir
if (!-d $opts->{'dir'}) {
	$out = &run_as_domain_user($d, "mkdir -p ".quotemeta($opts->{'dir'}));
	-d $opts->{'dir'} ||
		return (0, "Failed to create directory : <tt>$out</tt>.");
	}

# If possible, install the Jinja2 package globally from pip
if (&compare_versions($ver, 1.4) >= 0 && &has_command("pip")) {
	&system_logged("pip install Jinja2 >/dev/null 2>&1 </dev/null");
	&system_logged("pip install PyMySQL >/dev/null 2>&1 </dev/null");
	}

# Create python base dir
$ENV{'PYTHONPATH'} = "$opts->{'dir'}/lib/python";
&run_as_domain_user($d, "mkdir -p ".quotemeta($ENV{'PYTHONPATH'}));

# Extract the source, then install to the target dir
local $temp = &transname();
local $err = &extract_script_archive($files->{'source'}, $temp, $d);
$err && return (0, "Failed to extract Trac source : $err");
local $icmd = "cd ".quotemeta("$temp/Trac-$ver")." && ".
	  "$python setup.py install --home ".quotemeta($opts->{'dir'})." 2>&1";
local $out = &run_as_domain_user($d, $icmd);
if ($?) {
	return (-1, "Trac source install failed : ".
		    "<pre>".&html_escape($out)."</pre>");
	}

# Extract and copy the flup source
local $err = &extract_script_archive($files->{'flup'}, $temp, $d);
$err && return (-1, "Failed to extract flup source : $err");
local $out = &run_as_domain_user($d, 
	"cp -r ".quotemeta("$temp/flup-1.0/flup").
	" ".quotemeta("$opts->{'dir'}/lib/python"));
if ($?) {
	return (-1, "flup source copy failed : ".
		    "<pre>".&html_escape($out)."</pre>");
	}

if (!$upgrade) {
	# Fix database name
	if ($dbtype eq 'postgres') {
		$dbhost = "";
	}

	# Create the initial project
	local $projectdir = $opts->{'dir'}."/".$opts->{'project'};
	local $icmd = "cd ".quotemeta($opts->{'dir'})." && ".
		  "./bin/trac-admin ".quotemeta($projectdir).
		  " initenv ".quotemeta($opts->{'project'})." ".
		  $dbtype."://".quotemeta($dbuser).":".quotemeta($dbpass)."@".
		   quotemeta($dbhost)."/".quotemeta($dbname);
	if (&compare_versions($ver, 1.4) < 0) {
		$icmd .= " svn ".quotemeta($d->{'home'}).
		         "/svn/".quotemeta($opts->{'rep'});
		}
	$icmd .= " 2>&1 && ".
		 "./bin/trac-admin ".quotemeta($projectdir)." permission add ".
		 quotemeta($opts->{'tracadmin'})." TRAC_ADMIN 2>&1";
	local $out = &run_as_domain_user($d, $icmd);
	if ($?) {
		return (-1, "Project initialization install failed : ".
			    "<pre>".&html_escape($out)."</pre>");
		}

	# Fix trac.ini
	local $url = &script_path_url($d, $opts);
	local $sfile = "$projectdir/conf/trac.ini";
	-r $sfile || return (0, "Trac settings file $sfile was not found");
	local $lref = &read_file_lines_as_domain_user($d, $sfile);
	local $url = &script_path_url($d, $opts);
	local $adminpath = $opts->{'path'} eq "/" ?
		  "/admin" : "$opts->{'path'}/admin";
	my $i = 0;
	foreach my $l (@$lref) {
		if ($l =~ /authz_file\s*=/) {
			$l = "authz_file = $d->{'home'}/etc/svn-access.conf";
			}
		if ($l =~ /^url\s*=/) {
			$l = "url = $opts->{'path'}";
			}
		if ($l =~ /^base_url\s*=/) {
			$l = "base_url = $url";
			}
		if ($l =~ /^link\s*=/) {
			$l = "link = $opts->{'path'}";
			}
		if ($l =~ /authz_module_name\s*=/) {
			$l = "authz_module_name = $opts->{'rep'}";
			}
		if ($l =~ /src\s*=/) {
			$l = "src = common/trac_banner.png";
			}
		if ($l =~ /alt\s*=/) {
			$l = "alt = Trac logo";
			}
		if ($l =~ /^admin\s*=/) {
			$l = "admin = $adminpath";
			}
		$i++;
		}
	&flush_file_lines_as_domain_user($d, $sfile);
	}

# Create python fcgi wrapper script
local $fcgi = "$opts->{'dir'}/trac.fcgi";
local $wrapper = "$opts->{'dir'}/trac.fcgi.py";
&open_tempfile_as_domain_user($d, FCGI, ">$fcgi");
&print_tempfile(FCGI, "#!/bin/sh\n");
&print_tempfile(FCGI, "export PYTHONPATH=$opts->{'dir'}/lib/python\n");
&print_tempfile(FCGI, "exec $python $wrapper\n");
&close_tempfile_as_domain_user($d, FCGI);
&set_permissions_as_domain_user($d, 0755, $fcgi);

# Create python fcgi wrapper
if (!-r $wrapper) {
	&open_tempfile_as_domain_user($d, WRAPPER, ">$wrapper");
	&print_tempfile(WRAPPER, "#!$python\n");
	&print_tempfile(WRAPPER, "import sys, os\n");
	&print_tempfile(WRAPPER, "os.environ['TRAC_ENV'] = \"$opts->{'dir'}/$opts->{'project'}\"\n");
	&print_tempfile(WRAPPER, "os.environ['PYTHON_EGG_CACHE'] = \"$d->{'home'}/tmp\"\n");
	&print_tempfile(WRAPPER, "os.chdir(\"$opts->{'dir'}\")\n");
	&print_tempfile(WRAPPER, "from trac.web.main import dispatch_request\n");
	&print_tempfile(WRAPPER, "from flup.server.fcgi import WSGIServer\n");
	&print_tempfile(WRAPPER, "WSGIServer(dispatch_request).run()\n");
	&close_tempfile_as_domain_user($d, WRAPPER);
	&set_permissions_as_domain_user($d, 0755, $wrapper);
	}

# Add <Location> block to Apache config
&foreign_require("virtualmin-svn", "virtualmin-svn-lib.pl");
%sconfig = &foreign_config("virtualmin-svn");
$sconfig{'auth'} ||= "Basic";
local $conf = &apache::get_config();
local @ports = ( $d->{'web_port'},
		 $d->{'ssl'} ? ( $d->{'web_sslport'} ) : ( ) );
foreach my $port (@ports) {
	local ($virt, $vconf) = &get_apache_virtual($d->{'dom'}, $port);
	next if (!$virt);
	local $changed;
	# enable the rewrite engine for the whol virtual host, because only then
	# SCRIPT_URL gets populated that Trac uses to build the URL
	local @rewrite = &apache::find_directive("RewriteEngine", $vconf);
	local ($tr) = grep { $_ =~ /On/ } @rewrite;
	if (!$tr) {
		push(@rewrite, "On");
		&apache::save_directive("RewriteEngine", \@rewrite, $vconf, $conf);
		$changed++;
		}
	local @locs = &apache::find_directive_struct("Location", $vconf);
	local ($loc) = grep { $_->{'words'}->[0] eq $opts->{'path'} } @locs;
	local $reldir = $opts->{'dir'};
	$reldir =~ s/^\Q$d->{'home'}\/\E//;
	if (!$loc) {
		local $loc = { 'name' => 'Location',
				   'value' => $opts->{'path'},
				   'type' => 1,
				   'members' => [
				{ 'name' => 'AddHandler',
				  'value' => 'fcgid-script .fcgi' },
				{ 'name' => 'RewriteEngine',
				  'value' => 'On' },
				{ 'name' => 'RewriteCond',
				  'value' => '%{REQUEST_FILENAME} !-f' },
				{ 'name' => 'RewriteCond',
				  'value' => '%{REQUEST_FILENAME} !trac.fcgi' },
				{ 'name' => 'RewriteRule',
				  'value' => "$reldir(.*) trac.fcgi/\$1 [L]" },
				]
			};
		&apache::save_directive_struct(undef, $loc, $vconf, $conf);
		$changed++;
		}
	local $passwd_file = &virtualmin_svn::passwd_file($d);
	local $at = $sconfig{'auth'};
	local $auf = $at eq "Digest" && $apache::httpd_modules{'core'} < 2.2 ?
			"AuthDigestFile" : "AuthUserFile";
	local $adp = $at eq "Digest" && $apache::httpd_modules{'core'} >= 2.2 ?
			"AuthDigestProvider" : "";
	local $adv = $at eq "Digest" && $apache::httpd_modules{'core'} >= 2.2 ?
			"file" : "";
	local @locms = &apache::find_directive_struct("LocationMatch", $vconf);
	local $lpath = ($opts->{'path'} eq "/" ? "/" : $opts->{'path'}."/").
		       ".*/login";
	local ($login) = grep { $_->{'words'}->[0] eq $lpath } @locms;
	if (!$login) {
		local $login = { 'name' => 'LocationMatch',
				   'value' => $lpath,
				   'type' => 1,
				   'members' => [
				{ 'name' => 'AuthType',
				  'value' => "$at" },
				{ 'name' => 'AuthName',
				  'value' => "$d->{'dom'}" },
				{ 'name' => "$auf",
				  'value' => "$passwd_file" },
				{ 'name' => "$adp",
				  'value' => "$adv" },
				{ 'name' => 'Require',
				  'value' => 'valid-user' },
				]
			};
		&apache::save_directive_struct(undef, $login, $vconf, $conf);
		$changed++;
		}
	if ($changed) {
		&flush_file_lines($virt->{'file'});
		}
	}
&register_post_action(\&restart_apache);

# Run upgrade scripts
if ($upgrade) {
	my $cmd = "$opts->{'dir'}/bin/trac-admin $opts->{'dir'}/trac ".
		  "upgrade 2>&1";
	my $out = &run_as_domain_user($d, $cmd);
	if ($?) {
		return (-1, "Upgrade command failed : $out");
		}

	my $cmd = "$opts->{'dir'}/bin/trac-admin $opts->{'dir'}/trac ".
		  "wiki upgrade 2>&1";
	my $out = &run_as_domain_user($d, $cmd);
	if ($?) {
		return (-1, "Wiki upgrade command failed : $out");
		}
	}

local $url = &script_path_url($d, $opts);
local $rp = $opts->{'dir'};
$rp =~ s/^$d->{'home'}\///;
local @users = &list_domain_users($d, 0, 1, 1, 1);
local ($tracu) = grep { &remove_userdom($_->{'user'}, $d) eq $opts->{'tracadmin'} } @users;
return (1, "Initial Trac installation complete. Go to <a target=_blank href='$url'>$url</a> to manage it.", "Under $rp", $url, $opts->{'tracadmin'}, $tracu ? $tracu->{'plainpass'} : undef);
}

# script_trac_uninstall(&domain, version, &opts)
# Un-installs a Trac installation, by deleting the directory and database.
# Returns 1 on success and a message, or 0 on failure and an error
sub script_trac_uninstall
{
local ($d, $version, $opts) = @_;

# Remove the contents of the target directory
local $derr = &delete_script_install_directory($d, $opts);
return (0, $derr) if ($derr);

# Remove base Trac tables from the database
&cleanup_script_database($d, $opts->{'db'}, "trac_");
&cleanup_script_database($d, $opts->{'db'}, \@trac_tables);

# Remove <Location> and <LocationMatch> block
&require_apache();
local $conf = &apache::get_config();
local @ports = ( $d->{'web_port'},
		 $d->{'ssl'} ? ( $d->{'web_sslport'} ) : ( ) );
foreach my $port (@ports) {
	local ($virt, $vconf) = &get_apache_virtual($d->{'dom'}, $port);
	next if (!$virt);
	local @locs = &apache::find_directive_struct("Location", $vconf);
	local ($loc) = grep { $_->{'words'}->[0] eq $opts->{'path'} } @locs;
	if ($loc) {
		&apache::save_directive_struct($loc, undef, $vconf, $conf);
		}
	local @locms = &apache::find_directive_struct("LocationMatch", $vconf);
	local $lpath = ($opts->{'dir'} eq "/" ? "/" : $opts->{'dir'}."/").
		       ".*/login";
	local ($login) = grep { $_->{'words'}->[0] eq ".*/login" ||
				$_->{'words'}->[0] eq $lpath } @locms;
	if ($login) {
		&apache::save_directive_struct($login, undef, $vconf, $conf);
		}
	if ($login || $loc) {
		&flush_file_lines($virt->{'file'});
		}
	}
&register_post_action(\&restart_apache);

# Take out the DB
if ($opts->{'newdb'}) {
	&delete_script_database($d, $opts->{'db'});
	}

return (1, "Trac directory and tables deleted.");
}

# script_trac_latest(version)
# Returns a URL and regular expression or callback func to get the version
sub script_trac_latest
{
local ($ver) = @_;
if (&compare_versions($ver, "1.2") < 0) {
	return ( "http://trac.edgewall.org/wiki/TracDownload",
		 "Trac-(1\\.0\\.[0-9\\.]+).tar.gz" );
	}
elsif (&compare_versions($ver, "1.4") < 0) {
	return ( "http://trac.edgewall.org/wiki/TracDownload",
		 "Trac-(1\\.2\\.[0-9\\.]+).tar.gz" );
	}
else {
	return ( "http://trac.edgewall.org/wiki/TracDownload",
		 "Trac-(1\\.4\\.[0-9\\.]+).tar.gz" );
	}
return ( );
}

sub script_trac_site
{
return 'http://trac.edgewall.org/';
}

1;

