#!/usr/bin/perl
# Adds config file settings to record ikiwiki-hosting information,
# and contains essential hosting functionality.
package IkiWiki::Plugin::ikiwikihosting;

use warnings;
use strict;
use IkiWiki 3.00;
use URI::Escape;
			
sub import {
	hook(type => "getsetup", id => "ikiwikihosting",  call => \&getsetup);
	hook(type => "sessioncgi", id => "ikiwikihosting",  call => \&sessioncgi);
	hook(type => "formbuilder_setup", id => "ikiwikihosting",
		call => \&formbuilder_setup, last => 1);
	hook(type => "checkconfig", id => "ikiwikihosting",  call => \&checkconfig);
	hook(type => "genwrapper", id => "ikiwikihosting", call => \&genwrapper);
}

sub getsetup () {
	return
		plugin => {
			safe => 0,
			rebuild => undef,
			section => "core",
		},
		urlalias => {
			type => "string",
			example => [],
			description => "list of urls that alias to the main url",
			safe => 0,
			rebuild => 0,
		},
		owner => {
			type => "string",
			description => "openid or email of primary site owner",
			safe => 0,
			rebuild => 0,
		},
		parent => {
			type => "string",
			description => "optional hostname of site this one was branched from",
			safe => 0,
			rebuild => 0,
		},
		hostname => {
			type => "string",
			description => "internal hostname of this site",
			safe => 0,
			rebuild => 0,
		},
		created => {
			type => "integer",
			description => "site creation datestamp",
			safe => 0,
			rebuild => 0,
		},
		log_period => {
			type => "integer",
			example => 7,
			description => "how many days to retain logs",
			safe => 1,
			rebuild => 0,
		},
		ipv6_disabled => {
			type => "boolean",
			example => 1,
			description => "disable IPv6?",
			safe => 1,
			rebuild => 0,
		},
		redirect_to_https => {
			type => "boolean",
			example => 1,
			description => "redirect from http to https?",
			safe => 1,
			rebuild => 0,
		},
		use_letsencrypt => {
			type => "boolean",
			example => 1,
			description => "use Lets Encrypt to generate https certificate?",
			safe => 1,
			rebuild => 0,
		},
}

sub sessioncgi ($$) {
	my $cgi=shift;
	my $session=shift;

	return unless defined $cgi->param("do");
       
	if ($cgi->param("do") eq "deletesite") {
		deletesite($cgi, $session);
	}
	elsif ($cgi->param("do") eq "setupdns") {
		setupdns($cgi, $session);
	}
	elsif ($cgi->param("do") eq "setupsshkeys") {
		setupsshkeys($cgi, $session);
	}
	elsif ($cgi->param("do") eq "sshkey") {
		print "Content-Type: text/plain\n\n";
		print readfile(get_ssh_public_key())."\n";
		exit 0;
	}
}

sub is_admin_or_owner ($) {
	my $session=shift;

	IkiWiki::is_admin($session->param("name")) ||
	$config{owner} eq $session->param("name");
}

sub formbuilder_setup (@) {
	my %params=@_;

	my $form=$params{form};
	my $cgi=$params{cgi};

	if ($form->title eq "preferences" &&
	    is_admin_or_owner($params{session})) {
		if ($config{controlsitecgiurl}) {
			push @{$params{buttons}}, "Control Panel";
			if ($form->submitted && $form->submitted eq "Control Panel") {
				controlpanelredir($cgi);
			}
		}
		eval q{use IkiWiki::Hosting};
		error $@ if $@;
		IkiWiki::Hosting::readconfig();
		if ($config{allow_analog_reports}) {
			push @{$params{buttons}}, "Statistics";
			if ($form->submitted && $form->submitted eq "Statistics") {
				analog_report($cgi, $params{session});
			}
		}
	}
	if ($form->title eq "setup" &&
	    is_admin_or_owner($params{session})) {
		push @{$params{buttons}}, "DNS";
		if ($form->submitted && $form->submitted eq "DNS") {
			setupdns($cgi, $params{session});
		}
		push @{$params{buttons}}, "SSH keys";
		if ($form->submitted && $form->submitted eq "SSH keys") {
			setupsshkeys($cgi, $params{session});
		}
	}
	if (defined $cgi->param('welcome')) {
		eval q{use IkiWiki::Hosting};
		error $@ if $@;
		my $t=IkiWiki::Hosting::ikisite_template($cgi->param('welcome').".tmpl");
		$form->tmpl_param(message => $t->output);
	}

}

sub deletesite ($$){
	my $cgi=shift;
	my $session=shift;

	IkiWiki::needsignin($cgi, $session);
	if (! is_admin_or_owner($session)) {
		error "You are not logged in as an admin or site owner.";
	}

	eval q{use CGI::FormBuilder};
	error($@) if $@;
	my $f = CGI::FormBuilder->new(
		name => "deletesite",
		header => 0,
		charset => "utf-8",
		method => 'POST',
		javascript => 0,
		params => $cgi,
		action => IkiWiki::cgiurl(),
		stylesheet => 1,
		fields => [qw{do}],
	);      
		
	$f->field(name => "do", type => "hidden", value => "deletesite", force => 1);
	$f->field(name => "sid", type => "hidden", value => $session->id,
		force => 1);
	$f->title("Delete this site?");
	my @buttons=("Delete Entire Site", "Cancel");

	if ($f->submitted eq $buttons[1]) { # Cancel
		controlpanelredir($cgi);
	}
	elsif ($f->submitted eq $buttons[0] && $f->validate) {
		IkiWiki::checksessionexpiry($cgi, $session, $cgi->param('sid'));
		
		eval q{use IkiWiki::Hosting};
		error $@ if $@;
		
		# This is running as the site user, but ikisite delete
		# needs to run as root. So, use ikisite-wrapper to run
		# it, getting a nonce first.
		$ENV{IKISITE_NONCE}=IkiWiki::Hosting::getshell("ikisite",
			"createnonce", $config{hostname});
	
		# Drop the lock, to avoid any interference with site
		# deletion.
		IkiWiki::unlockwiki();

		IkiWiki::Hosting::shell("ikisite-wrapper", "delete", $config{hostname});

		# Have to take the user somewhere now that their
		# site is gone..
		controlpanelredir($cgi);
	}
	else {
		IkiWiki::showform($f, \@buttons, $session, $cgi);
	}
	exit 0;
}

sub setupdns ($$){
	my $cgi=shift;
	my $session=shift;

	IkiWiki::needsignin($cgi, $session);
	if (! is_admin_or_owner($session)) {
		error "You are not logged in as an admin or site owner.";
	}
	
	eval q{use IkiWiki::Hosting};
	error $@ if $@;
	my $template=IkiWiki::Hosting::ikisite_template("setupdns.tmpl");
	$template->param(internal_hostname => $config{hostname});
	$template->param(sid => $session->id);

	if ($cgi->param('submit')) {
		IkiWiki::checksessionexpiry($cgi, $session, $cgi->param('sid'));

		my $external=$cgi->param('external');
		my @alias=split(' ', $cgi->param('alias'));
		my %alias=map { $_ => 1 } @alias;
		if (lc $external ne lc $config{hostname}) {
			if ($external =~ /^www\.(.*)$/i) {
				$alias{$1}=1;
			}
			else {
				$alias{"www.$external"}=1;
			}
		}

		# This is running as the site user, but ikisite domains
		# needs to run as root. So, use ikisite-wrapper to run
		# it, getting a nonce first.
		$ENV{IKISITE_NONCE}=IkiWiki::Hosting::getshell("ikisite",
			"createnonce", $config{hostname});
	
		# Drop the lock, to avoid interference with site
		# rebuild.
		IkiWiki::unlockwiki();

		# Gather any error output from ikisite domains, to display
		# to the user.
		eval q{use IPC::Open3};
		error $@ if $@;
		my $pid=open3(undef, \*STDIN, \*STDIN,
			"ikisite-wrapper", "domains", $config{hostname},
			(lc $external ne lc $config{hostname} ? "--external=$external" : ()),
			(map { "--alias=$_" } keys %alias),
		);
		my $error;
		{
			local $/=undef;
			$error=<STDIN>;
			$error=~s/\n/<br \/>/g;
		}
		waitpid( $pid, 0);
		my $ret=$?;
		IkiWiki::Hosting::shell("ikisite-wrapper", "deletenonce",
			$config{hostname}, "--nonce=$ENV{IKISITE_NONCE}");
		delete $ENV{IKISITE_NONCE};
		
		if (! $ret) {
			$template->param(dns_ok => 1);
			$template->param(external_hostname => external_hostname($config{hostname}));
			$template->param(alias => join("\n", alias($config{hostname}))."\n");
		}
		else {
			$template->param(error => $error);
			# preserve bad input values to allow correcting
			$template->param(external_hostname => $external);
			$template->param(alias => join("\n", @alias)."\n");
			# special exit codes used for missing and wrong dns
			if ($ret >> 8 == 2) {
				$template->param(dns_needed => 1);
			}
			elsif ($ret >> 8 == 3) {
				$template->param(dns_wrong => 1);
			}
		}
	}
	else {
		$template->param(first => 1);
		$template->param(external_hostname => external_hostname($config{hostname}));
		$template->param(alias => join("\n", alias($config{hostname}))."\n");
	}

	IkiWiki::printheader($session);
	print IkiWiki::cgitemplate($cgi, "DNS", $template->output);
	exit 0;
}

sub setupsshkeys ($$) {
	my $cgi=shift;
	my $session=shift;

	IkiWiki::needsignin($cgi, $session);
	if (! is_admin_or_owner($session)) {
		error "You are not logged in as an admin or site owner.";
	}
	
	eval q{use IkiWiki::Hosting};
	error $@ if $@;
	my $template=IkiWiki::Hosting::ikisite_template("setupsshkeys.tmpl");
	$template->param(sid => $session->id);

	my $changed=0;
	if ($cgi->param('submit')) {
		IkiWiki::checksessionexpiry($cgi, $session, $cgi->param('sid'));

		if (defined $cgi->param("add") &&
		    length $cgi->param("add")) {
			eval { IkiWiki::Hosting::shell("ikisite",
				"enablesshkey", $config{hostname},
				"--key=".$cgi->param("add"),
			) };
			if ($@) {
				$template->param(add_error => "Unable to add SSH key. Check format.");
				$template->param(add => $cgi->param("add"));
			}
			else {
				$changed=1;
			}
		}
	}

	if (defined $cgi->param("delete") &&
	    length $cgi->param("delete")) {
		print STDERR "delete $@ ".$cgi->param("delete")."\n";
		eval { IkiWiki::Hosting::shell("ikisite",
			"disablesshkey", $config{hostname},
			"--key=".$cgi->param("delete"),
		) };
		if ($@) {
			$template->param(delete_error => "Unable to delete SSH key.");
		}
		else {
			$changed=1;
		}
	}
	
	my @keys=split "\n", IkiWiki::Hosting::getshell("ikisite", "sshkeys", $config{hostname});
	$template->param(keys => [map {
		key => $_,
		key_urlencoded => uri_escape($_),
	}, grep defined, @keys]);

	# Ikiwiki will be running as the site's username, 
	my $username=(getpwuid($<))[0]; 
	if (defined $username && length $username && 
	    defined $config{hostname} && length $config{hostname}) { 
		$template->param(sshaddress => "$username\@$config{hostname}");
	} 

	IkiWiki::printheader($session);
	print IkiWiki::cgitemplate($cgi, "SSH keys", $template->output);

	handlechangedsetup() if $changed;
	exit 0;
}


sub controlpanelredir ($) {
	my $cgi=shift;

	eval q{use IkiWiki::Hosting};
	error $@ if $@;
	IkiWiki::Hosting::readconfig();

	unless ($config{controlsitecgiurl}) {
		error "This wiki does not have a control panel.";
	}

	IkiWiki::redirect($cgi, $config{controlsitecgiurl}."?do=controlpanel");
}

sub external_hostname ($) {
	my $internal_hostname=shift;

	eval q{use URI};
	error $@ if $@;
	eval q{use IkiWiki::Hosting};
	error $@ if $@;
	my $url=IkiWiki::Hosting::getshell("ikisite", "getsetup",
			$internal_hostname, "url");
	my $u=URI->new($url);
	if (! $u->can("host")) {
		error "bad url for $internal_hostname: $url\n";
	}

	return $u->host;
}

sub alias ($) {
	my $internal_hostname=shift;

	eval q{use URI};
	error $@ if $@;
	eval q{use IkiWiki::Hosting};
	error $@ if $@;
	my $alias=IkiWiki::Hosting::yamlgetshell("ikisite", "getsetup",
		$internal_hostname, "urlalias");
	my @alias;
	if ($alias) {
		foreach my $u (map { URI->new($_) } @$alias) {
			if ($u->can("host")) {
				push @alias, $u->host;
			}
		}
	}
	return @alias;
}

sub checkconfig {
	if (! defined $config{ipv6_disabled}) {
		$config{ipv6_disabled}=0;
	}
	if (! defined $config{redirect_to_https}) {
		$config{redirect_to_https}=0;
	}
}

sub genwrapper {
	# This hook is called whenever wrappers are being built,
	# and that happens when configuration has changed.
	handlechangedsetup();

	# C code to inject into wrapper.
	return "";
}

sub handlechangedsetup {
	# Locking requires this be done in the background.
	eval q{use IkiWiki::Hosting};
	error $@ if $@;
	my $pid=IkiWiki::Hosting::daemonize();
	if ($pid) {
		return;
	}
	
	# Drop lock; ikisite takes the lock itself.
	IkiWiki::unlockwiki();

	# Commit any changes to the setup file into the setup branch.
	IkiWiki::Hosting::getshell("ikisite", "commitsetup", $config{hostname});

	# Re-enable the site for changes to eg, redirect_to_https or dns.
	# This can't enable any site that's not enabled, since the site has
	# to be enabled already for this cgi to be run.
	IkiWiki::Hosting::getshell("ikisite-wrapper", "enable", $config{hostname});
	
	exit;
}

# Returns path of ssh public key.
# If no key exists, one is created. (Can be a little slow.)
sub get_ssh_public_key {
	my @keys;
	my $findkeys=sub { @keys=glob("$ENV{HOME}/.ssh/id_*.pub") };
	$findkeys->();
	if (! @keys) {
		my $ret=system("ssh-keygen", "-q", "-t", "rsa",
			"-f", "$ENV{HOME}/.ssh/id_rsa",
			"-N", "",
			"-C", $config{wikiname});
			$findkeys->();
		if ($ret !=0 || ! @keys) {
			error "ssh-keygen failed";
		}
	}
	return shift(@keys);
}

sub analog_report ($$) {
	my $cgi=shift;
	my $session=shift;

	IkiWiki::needsignin($cgi, $session);
	if (! is_admin_or_owner($session)) {
		error "You are not logged in as an admin or site owner.";
	}
	
	print "Content-type: text/html\n\n";
	system("ikisite", $config{hostname}, "analog", "--"
		, "-a" # html output
		, "+CHOSTNAME 'this site'"
	);
	exit 0;
}

1
