#!/usr/bin/perl

# Copyright (C) 2007-2023 X2Go Project - https://wiki.x2go.org
# Copyright (C) 2007-2023 Oleksandr Shneyder <o.shneyder@phoca-gmbh.de>
# Copyright (C) 2007-2023 Heinz-Markus Graesing <heinz-m.graesing@obviously-nice.de>
# Copyright (C) 2017-2018 Walid Moghrabi <w.moghrabi@servicemagic.eu>
# Copyright (C) 2018-2023 Mihai Moldovan <ionic@ionic.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

use strict;
use Sys::Syslog qw( :standard :macros );
use File::Path;
use Getopt::Long;
use DBI;
use Try::Tiny;

use X2Go::Config qw( get_sqlconfig );
use X2Go::Log qw( loglevel );

openlog($0,'cons,pid','user');
setlogmask( LOG_UPTO(loglevel()) );

sub show_usage()
{
	print "X2Go SQL admin interface. Use it to create x2go database and insert or remove users or groups in x2go database\n".
	      "Usage:\nx2godbadmin --createdb\n".
	      "x2godbadmin --updatedb\n".
	      "x2godbadmin --listusers\n".
	      "x2godbadmin --adduser|rmuser <UNIX user>\n".
	      "x2godbadmin --addgroup|rmgroup <UNIX group>\n";
}

my $help='';
my $createdb='';
my $updatedb='';
my $adduser='';
my $rmuser='';
my $addgroup='';
my $rmgroup='';
my $listusers='';

GetOptions('listusers' => \$listusers, 'createdb' => \$createdb, 'updatedb' => \$updatedb, 'help' => \$help, 'adduser=s' => \$adduser,
           'addgroup=s' => \$addgroup, 'rmuser=s' => \$rmuser, 'rmgroup=s' => \$rmgroup);

if ($help || ! ( $createdb || $updatedb || $adduser || $rmuser || $addgroup || $rmgroup || $listusers))
{
	show_usage();
	exit(0);
}

my $Config = get_sqlconfig();

if ($Config->param("backend") eq 'sqlite')
{
	my $user="x2gouser";
	my ($name, $pass, $uid, $pgid, $quota, $comment, $gcos, $dir, $shell, $expire) = getpwnam($user) or die ("Can not find user ($user)\n");
	my $dbfile="$dir/x2go_sessions";

	if ($listusers|| $adduser||$addgroup||$rmuser||$rmgroup)
	{
		print "Only \"--createdb\" and \"--updatedb\" options are available with SQLite3 backend\n";
		exit(0);
	}
	if ($updatedb)
	{
		if ( ! -e $dbfile)
		{
			$createdb = 1;
		}
		else
		{
			# Update the session DB file
			my $dbh=DBI->connect("dbi:SQLite:dbname=$dbfile","","",{AutoCommit => 1, PrintError => 0, PrintWarn => 1, RaiseError => 1 }) or die $_;
			my $coltest;
			my $sth;
			# Update missing column on sessions table: tekictrl_port
			try
			{
				$dbh->prepare("
				              select tekictrl_port from sessions
				              ");
			}
			catch
			{
				print "ADDING: tekictrl_port column to table sessions\n";
				$sth=$dbh->prepare("alter table sessions add column 'tekictrl_port' int");
				$sth->execute();
				$sth->finish();
			};

			# Update missing column on sessions table: tekidata_port
			try
			{
				$dbh->prepare("select tekidata_port from sessions");
			}
			catch
			{
				print "ADDING: tekidata_port column to table sessions\n";
				$sth=$dbh->prepare("alter table sessions add column 'tekidata_port' int");
				$sth->execute();
				$sth->finish();
			};

			exit(0);
		}
	}

	if ($createdb)
	{
		if (! -d "$dir" )
		{
			if ( defined (&File::Path::make_path) )
			{
				File::Path::make_path("$dir");
			}
			elsif ( defined (&File::Path::mkpath) )
			{
				File::Path::mkpath("$dir");
			}
			else
			{
				die "Unable to create folders with File::Path";
			}
		}
		if ( -e $dbfile)
		{
			unlink($dbfile);
		}
		my $dbh=DBI->connect("dbi:SQLite:dbname=$dbfile","","",{AutoCommit => 1}) or die $_;

		my $sth=$dbh->prepare("
		                      create table sessions(
		                      session_id varchar(500) primary key,
		                      display integer not null,
		                      uname varchar(100) not null,
		                      server varchar(100) not null,
		                      client inet,
		                      status char(1) not null default 'R',
		                      init_time timestamp not null default CURRENT_TIMESTAMP,
		                      last_time timestamp not null default CURRENT_TIMESTAMP,
		                      cookie char(33),
		                      agent_pid int,
		                      gr_port int,
		                      sound_port int,
		                      fs_port int,
		                      tekictrl_port int,
		                      tekidata_port int,
		                      unique(display))
		                      ");
		$sth->execute() or die;
		$sth->finish();

		my $sth=$dbh->prepare("create table messages(mess_id varchar(20) primary key, message text)");
		$sth->execute() or die;
		$sth->finish();

		my $sth=$dbh->prepare("
		                      create table user_messages(
		                      mess_id varchar(20) not null,
		                      uname varchar(100) not null)
		                      ");
		$sth->execute() or die;
		$sth->finish();

		my $sth=$dbh->prepare("
		                      create table used_ports(
		                      server varchar(100) not null,
		                      session_id varchar(500) references sessions on delete cascade,
		                      port integer primary key)
		                      ");
		$sth->execute() or die;
		$sth->finish();

		my $sth=$dbh->prepare("
		                      create table mounts(
		                      session_id varchar(500) references sessions on delete restrict,
		                      path varchar(512) not null,
		                      client inet not null,
		                      primary key(path,client))
		                      ");
		$sth->execute() or die;
		$sth->finish();

		my $sth=$dbh->prepare("
		                      CREATE TRIGGER fkd_mounts_session_id
		                      BEFORE DELETE ON sessions
		                      FOR EACH ROW BEGIN
		                      SELECT CASE
		                      WHEN ((SELECT session_id FROM mounts WHERE session_id = OLD.session_id) IS NOT NULL)
		                      THEN RAISE(ABORT, 'delete on table \"sessions\" violates foreign key on table \"mounts\"')
		                      END;
		                      END;
		                      ");
		$sth->execute() or die;
		$sth->finish();

		my $sth=$dbh->prepare("
		                      CREATE TRIGGER fkd_ports_session_id
		                      BEFORE DELETE ON sessions
		                      FOR EACH ROW
		                      BEGIN
		                      DELETE FROM used_ports WHERE session_id = OLD.session_id;
		                      END;
		                      END;
		                      ");
		$sth->execute() or die;
		$sth->finish();

		# undef $dbh should be preferred to $dbh->disconnect(), see
		# https://www.perlmonks.org/?node_id=665714
		undef $dbh;
		chmod(0770, "$dir");
		chown('root',$pgid,"$dir");
		chmod(0660, "$dbfile");
		chown('root',$pgid,"$dbfile");

		exit(0);
	}
}

if ($Config->param("backend") eq 'postgres')
{
	my $host=$Config->param("postgres.host");
	my $port=$Config->param("postgres.port");
	my $sslmode=$Config->param("postgres.ssl");
	if (!$sslmode)
	{
		$sslmode="prefer";
	}
	my $dbadmin=$Config->param("postgres.dbadmin");
	my $x2goadmin="x2godbuser";
	my $x2goadminpass=`pwgen -s -c -n 32 1`;
	chomp ($x2goadminpass);
	my $db="x2go_sessions";

	if (!$host)
	{
		$host='localhost';
	}
	if (!$port)
	{
		$port='5432';
	}
	if (!$dbadmin)
	{
		$dbadmin='postgres';
	}

	open (FL,"< /etc/x2go/x2gosql/passwords/pgadmin") or die "Can't read password file /etc/x2go/x2gosql/passwords/pgadmin";
	my $dbadminpass=<FL>;
	close(FL);
	chomp($dbadminpass);

	if ($updatedb)
	{
		# check if the DB already exists, if not, create it...
		my $dbh;
		until (
			$dbh = DBI->connect("dbi:Pg:dbname=$db;host=$host;port=$port;sslmode=$sslmode", "$dbadmin", "$dbadminpass",{AutoCommit => 1, RaiseError => 0, PrintError => 0})
		)
		{
			$createdb = 1;
			last;
		};
		if (!$createdb) {
			$dbh = DBI->connect("dbi:Pg:dbname=$db;host=$host;port=$port;sslmode=$sslmode", "$dbadmin", "$dbadminpass",{AutoCommit => 1});
			if ($dbh) {
				my $update_views_n_rules = 0;
				my $sth_tekictrl;
				my $sth_tekidata;
				my $sth_update;
				try
				{
					$sth_tekictrl = $dbh->prepare("select tekictrl_port from sessions");
					$sth_tekictrl->execute();
				}
				catch
				{
					print "ADDING: tekictrl_port column to table sessions\n";
					$sth_tekictrl = $dbh->prepare("
					                              alter table sessions
					                              add column tekictrl_port int
					                              ");
					$sth_tekictrl->execute() or die;
					$sth_tekictrl->finish();
					$update_views_n_rules = 1;
				};
				try
				{
					$sth_tekidata = $dbh->prepare("select tekidata_port from sessions");
					$sth_tekidata->execute();
				}
				catch
				{
					print "ADDING: tekidata_port column to table sessions\n";
					$sth_tekidata=$dbh->prepare("
					                            alter table sessions
					                            add column tekidata_port int
					                            ");
					$sth_tekidata->execute() or die;
					$sth_tekidata->finish();
					$update_views_n_rules = 1;
				};

				if ($update_views_n_rules)
				{
					print "UPDATING VIEW: sessions_view\n";
					$sth_update=$dbh->prepare("
					                          create or replace VIEW sessions_view as
					                          SELECT
					                          agent_pid, session_id, display, server, status, init_time, cookie, client, gr_port,
					                          sound_port, last_time, uname, fs_port, tekictrl_port, tekidata_port from sessions
					                          where creator_id = current_user
					                          ");
					$sth_update->execute() or die;
					print "UPDATING RULE: update_sess_view\n";
					$sth_update=$dbh->prepare("
					                          create or replace RULE update_sess_view AS ON UPDATE
					                          TO sessions_view DO INSTEAD
					                          update sessions set
					                          status=NEW.status,
					                          last_time=NEW.last_time,
					                          cookie=NEW.cookie,
					                          agent_pid=NEW.agent_pid,
					                          client=NEW.client,
					                          gr_port=NEW.gr_port,
					                          sound_port=NEW.sound_port,
					                          fs_port=NEW.fs_port,
					                          tekictrl_port=NEW.tekictrl_port,
					                          tekidata_port=NEW.tekidata_port
					                          where session_id=OLD.session_id and creator_id=current_user
					                          ");
					$sth_update->execute() or die;
					$sth_update->finish();
				}
			}
			if ($dbh) {
				undef $dbh;
			}
			exit(0);
		}
		else
		{
			print "No session DB found. Use --createdb instead of --updatedb.\n";
		}
	}

	if ($createdb)
	{
		create_database($host, $port, $dbadmin, $dbadminpass, $db, $x2goadmin, $x2goadminpass, $sslmode);
		create_tables($host, $port, $dbadmin, $dbadminpass, $db, $x2goadmin, $sslmode);
		exit(0);
	}

	if ($listusers)
	{
		list_users($host, $port, $dbadmin, $dbadminpass, $sslmode);
		exit(0);
	}

	if ($adduser)
	{
		add_user($host, $port, $dbadmin, $dbadminpass, $db, $adduser, $sslmode);
	}

	if ($addgroup)
	{
		my ($name, $passwd, $gid, $members) = getgrnam( $addgroup);
		my @grp_members=split(' ',$members);
		foreach (@grp_members)
		{
			chomp($_);
			add_user($host, $port, $dbadmin, $dbadminpass, $db, $_, $sslmode);
		}
	}

	if ($rmuser)
	{
		rm_user($host, $port, $dbadmin, $dbadminpass, $db, $rmuser, $sslmode);
	}

	if ($rmgroup)
	{
		my ($name, $passwd, $gid, $members) = getgrnam( $rmgroup);
		my @grp_members=split(' ',$members);
		foreach (@grp_members)
		{
			chomp($_);
			rm_user($host, $port, $dbadmin, $dbadminpass, $db, $_, $sslmode);
		}
	}
}

if ($Config->param("backend") eq 'mysql')
{
	die "Backend currently not implemented!";

	my $host=$Config->param("mysql.host");
	my $port=$Config->param("mysql.port");
	my $unet=$Config->param("mysql.unet");
	my $dbadmin=$Config->param("mysql.dbadmin");
	my $x2goadmin="x2godbuser";
	my $x2goadminpass=`pwgen 8 1`;
	chomp ($x2goadminpass);
	my $db="x2go_sessions";

	if (!$host)
	{
		$host='localhost';
	}
	if (!$port)
	{
		$port='3306';
	}
	if (!$dbadmin)
	{
		$dbadmin='root';
	}

	open (FL,"< /etc/x2go/x2gosql/passwords/mysqladmin") or die "Can't read password file /etc/x2go/x2gosql/passwords/mysqladmin";
	my $dbadminpass=<FL>;
	close(FL);
	chomp($dbadminpass);

	if ($updatedb)
	{
		# check if the DB already exists, if not, create it...
		my $dbh;
		until (
			$dbh = DBI->connect("dbi:mysql:database=$db;host=$host;port=$port", "$dbadmin", "$dbadminpass", {AutoCommit => 1, RaiseError => 0, PrintError => 0})
		)
		{
			$createdb = 1;
			last;
		};
		if (!$createdb) {
			$dbh = DBI->connect("dbi:mysql:database=$db;host=$host;port=$port", "$dbadmin", "$dbadminpass", {AutoCommit => 1});
			if ($dbh) {
				# Intentionally left blank.
				# MySQL support was only added in X2Go Server 4.1.0.x, while SQLite and PostgreSQL support
				# have been around prior to 4.1.0.0, which is the first version that introduced new teki-related
				# columns.
			}
			if ($dbh) {
				undef $dbh;
			}
			exit(0);
		}
		else
		{
			print "No session DB found. Use --createdb instead of --updatedb.\n";
		}
	}

	if ($createdb)
	{
		create_database($host, $port, $dbadmin, $dbadminpass, $db, $x2goadmin, $x2goadminpass, $unet);
		create_tables($host, $port, $dbadmin, $dbadminpass, $db, $x2goadmin, $unet);
		exit(0);
	}

	if ($listusers)
	{
		list_users($host, $port, $dbadmin, $dbadminpass);
		exit(0);
	}

	if ($adduser)
	{
		add_user($host, $port, $dbadmin, $dbadminpass, $db, $adduser, $unet);
	}

	if ($addgroup)
	{
		my ($name, $passwd, $gid, $members) = getgrnam($addgroup);
		my @grp_members=split(' ',$members);
		foreach (@grp_members)
		{
			chomp($_);
			add_user($host, $port, $dbadmin, $dbadminpass, $db, $_, $unet);
		}
	}

	if ($rmuser)
	{
		rm_user($host, $port, $dbadmin, $dbadminpass, $db, $rmuser, $unet);
	}

	if ($rmgroup)
	{
		my ($name, $passwd, $gid, $members) = getgrnam($rmgroup);
		my @grp_members=split(' ',$members);
		foreach (@grp_members)
		{
			chomp($_);
			rm_user($host, $port, $dbadmin, $dbadminpass, $db, $_, $unet);
		}
	}
}

sub create_tables
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";
	my $db = shift or die "No db parameter supplied";
	my $x2goadmin = shift or die "No x2goadmin (user-level database username) parameter supplied";

	my $sslmode = undef;
	my $unet = undef;

	if ($Config->param("backend") eq 'postgres')
	{
		$sslmode = shift or die "No sslmode parameter supplied";

		pg_create_tables($host, $port, $dbadmin, $dbadminpass, $db, $x2goadmin, $sslmode);
	}
	elsif ($Config->param("backend") eq 'mysql')
	{
		$unet = shift or die "No unet parameter supplied";

		mysql_create_tables($host, $port, $dbadmin, $dbadminpass, $db, $x2goadmin, $unet);
	}
	else
	{
		die "Invalid database backend";
	}
}

sub pg_create_tables
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";
	my $db = shift or die "No db parameter supplied";
	my $x2goadmin = shift or die "No x2goadmin (user-level database username) parameter supplied";
	my $sslmode = shift or die "No sslmode parameter supplied";

	my $dbh = DBI->connect("dbi:Pg:dbname=$db;host=$host;port=$port;sslmode=$sslmode", "$dbadmin", "$dbadminpass", {AutoCommit => 1}) or die $_;

	my $sth=$dbh->prepare("
	                      create table sessions(
	                      session_id text primary key,
	                      display integer not null,
	                      uname text not null,
	                      server text not null,
	                      client inet,
	                      status char(1) not null default 'R',
	                      init_time timestamp not null default now(),
	                      last_time timestamp not null default now(),
	                      cookie char(33),
	                      agent_pid int,
	                      gr_port int,
	                      sound_port int,
	                      fs_port int,
	                      tekictrl_port int,
	                      tekidata_port int,
	                      creator_id text NOT NULL default current_user,
	                      unique(display))
	                      ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create VIEW sessions_view as
	                   SELECT
	                   agent_pid, session_id, display, server, status, init_time, cookie, client, gr_port,
	                   sound_port, last_time, uname, fs_port, tekictrl_port, tekidata_port from sessions
	                   where creator_id = current_user
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create VIEW servers_view as
	                   SELECT
	                   server, display, status from sessions
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create or replace RULE update_sess_priv AS ON UPDATE
	                   TO sessions where (OLD.creator_id <> current_user or OLD.creator_id <> NEW.creator_id) and current_user <> '$x2goadmin'
	                   DO INSTEAD NOTHING
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create or replace RULE insert_sess_priv AS ON INSERT
	                   TO sessions where NEW.creator_id <> current_user and current_user <> '$x2goadmin'
	                   DO INSTEAD NOTHING
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create or replace RULE delete_sess_priv AS ON DELETE
	                   TO sessions where OLD.creator_id <> current_user and current_user <> '$x2goadmin'
	                   DO INSTEAD NOTHING
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create or replace RULE update_sess_view AS ON UPDATE
	                   TO sessions_view DO INSTEAD
	                   update sessions set
	                   status=NEW.status,
	                   last_time=NEW.last_time,
	                   cookie=NEW.cookie,
	                   agent_pid=NEW.agent_pid,
	                   client=NEW.client,
	                   gr_port=NEW.gr_port,
	                   sound_port=NEW.sound_port,
	                   fs_port=NEW.fs_port,
	                   tekictrl_port=NEW.tekictrl_port,
	                   tekidata_port=NEW.tekidata_port
	                   where session_id=OLD.session_id and creator_id=current_user
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("create table messages(mess_id varchar(20) primary key, message text)");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create table user_messages(
	                   mess_id text not null,
	                   uname text not null)
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create table used_ports(
	                   server text not null,
	                   session_id text references sessions on delete cascade,
	                   creator_id text NOT NULL default current_user,
	                   port integer primary key)
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create VIEW ports_view as
	                   SELECT
	                   server, port from used_ports
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create or replace RULE insert_port_priv AS ON INSERT
	                   TO used_ports where NEW.creator_id <> current_user and current_user <> '$x2goadmin'
	                   DO INSTEAD NOTHING
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create or replace RULE update_port_priv AS ON UPDATE
	                   TO used_ports where (NEW.creator_id <> current_user or OLD.creator_id <> current_user) and current_user <> '$x2goadmin'
	                   DO INSTEAD NOTHING
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create or replace RULE delete_port_priv AS ON DELETE
	                   TO used_ports where OLD.creator_id <> current_user and current_user <> '$x2goadmin'
	                   DO INSTEAD NOTHING
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create table mounts(
	                   session_id text references sessions on delete restrict,
	                   path text not null,
	                   client inet not null,
	                   creator_id text NOT NULL default current_user,
	                   primary key(path,client))
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create VIEW mounts_view as
	                   SELECT
	                   client,path, session_id from mounts
	                   where creator_id = current_user
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create or replace RULE delete_mounts_view AS ON DELETE
	                   TO mounts_view DO INSTEAD
	                   delete from mounts
	                   where session_id=OLD.session_id and creator_id=current_user and path=OLD.path
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create or replace RULE insert_mount_priv AS ON INSERT
	                   TO mounts where NEW.creator_id <> current_user and current_user <> '$x2goadmin'
	                   DO INSTEAD NOTHING
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create or replace RULE update_mount_priv AS ON UPDATE
	                   TO mounts where (NEW.creator_id <> current_user or OLD.creator_id <> current_user) and current_user <> '$x2goadmin'
	                   DO INSTEAD NOTHING
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   create or replace RULE delete_mount_priv AS ON DELETE
	                   TO mounts where OLD.creator_id <> current_user and current_user <> '$x2goadmin'
	                   DO INSTEAD NOTHING
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("GRANT ALL PRIVILEGES ON sessions, messages, user_messages, used_ports, mounts TO $x2goadmin");
	$sth->execute() or die;
	$sth->finish();
	undef $dbh;
}

sub mysql_create_tables
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";
	my $db = shift or die "No db parameter supplied";
	my $x2goadmin = shift or die "No x2goadmin (user-level database username) parameter supplied";
	my $unet = shift or die "No unet parameter supplied";

	my $dbh = DBI->connect("dbi:mysql:database=$db;host=$host;port=$port", "$dbadmin", "$dbadminpass", {AutoCommit => 1}) or die $_;

	my $sth=$dbh->prepare("
	                      CREATE TABLE `sessions` (
	                      `session_id` varchar(2048) NOT NULL,
	                      `display` int NOT NULL,
	                      `uname` varchar(100) NOT NULL,
	                      `server` varchar(512) NOT NULL,
	                      `client` varchar(45) DEFAULT NULL,
	                      `status` char(1) NOT NULL DEFAULT 'R',
	                      `init_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
	                      `last_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
	                      `cookie` char(33) DEFAULT NULL,
	                      `agent_pid` int DEFAULT NULL,
	                      `gr_port` int DEFAULT NULL,
	                      `sound_port` int DEFAULT NULL,
	                      `fs_port` int DEFAULT NULL,
	                      `tekictrl_port` int DEFAULT NULL,
	                      `tekidata_port` int DEFAULT NULL,
	                      PRIMARY KEY (`session_id`),
	                      UNIQUE KEY `idx_sessions_display_server` (`display`,`server`))
	                      ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   CREATE TABLE `messages` (
	                   `mess_id` varchar(20) NOT NULL,
	                   `mesage` longtext,
	                   PRIMARY KEY (`mess_id`))
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   CREATE TABLE `user_messages` (
	                   `mess_id` varchar(20) NOT NULL,
	                   `uname` varchar(100) NOT NULL,
	                   PRIMARY KEY (`mess_id`))
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   CREATE TABLE `used_ports` (
	                   `server` varchar(512) NOT NULL,
	                   `session_id` varchar(2048) DEFAULT NULL,
	                   `port` int NOT NULL,
	                   PRIMARY KEY (`port`),
	                   KEY `used_ports_session_id_fkey_idx` (`session_id`),
	                   CONSTRAINT `used_ports_session_id_fkey` FOREIGN KEY (`session_id`) REFERENCES `sessions` (`session_id`) ON DELETE CASCADE ON UPDATE NO ACTION)
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("
	                   CREATE TABLE `mounts` (
	                   `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
	                   `session_id` varchar(2048) DEFAULT NULL,
	                   `path` varchar(2048) NOT NULL,
	                   `client` varchar(45) NOT NULL,
	                   PRIMARY KEY (`id`),
	                   UNIQUE KEY `mounts_pkey` (`path`,`client`),
	                   KEY `mounts_session_id_fkey_idx` (`session_id`),
	                   CONSTRAINT `mounts_session_id_fkey` FOREIGN KEY (`session_id`) REFERENCES `sessions` (`session_id`) ON DELETE RESTRICT ON UPDATE NO ACTION)
	                   ");
	$sth->execute() or die;

	$sth=$dbh->prepare("GRANT ALL PRIVILEGES ON `sessions`, `messages`, `user_messages`, `used_ports`, `mounts` TO `$x2goadmin`@`$unet`");
	$sth->execute() or die;
	$sth->finish();
	undef $dbh;
}

sub create_database
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";
	my $db = shift or die "No db parameter supplied";
	my $x2goadmin = shift or die "No x2goadmin (user-level database username) parameter supplied";
	my $x2goadminpass = shift or die "No x2goadminpass (user-level database pasword) parameter supplied";

	my $sslmode = undef;
	my $unet = undef;

	if ($Config->param("backend") eq 'postgres')
	{
		$sslmode = shift or die "No sslmode parameter supplied";

		pg_create_database($host, $port, $dbadmin, $dbadminpass, $db, $x2goadmin, $x2goadminpass, $sslmode);
	}
	elsif ($Config->param("backend") eq 'mysql')
	{
		$unet = shift or die "No unet parameter supplied";

		mysql_create_database($host, $port, $dbadmin, $dbadminpass, $db, $x2goadmin, $x2goadminpass, $unet);
	}
	else
	{
		die "Invalid database backend";
	}
}

sub pg_create_database
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";
	my $db = shift or die "No db parameter supplied";
	my $x2goadmin = shift or die "No x2goadmin (user-level database username) parameter supplied";
	my $x2goadminpass = shift or die "No x2goadminpass (user-level database pasword) parameter supplied";
	my $sslmode = shift or die "No sslmode parameter supplied";

	my $dbh=DBI->connect("dbi:Pg:dbname=postgres;host=$host;port=$port;sslmode=$sslmode", "$dbadmin", "$dbadminpass", {AutoCommit => 1}) or die $_;
	#drop db if exists
	my $sth=$dbh->prepare("drop database if exists $db");
	$sth->execute();
	#drop x2goadmin
	$sth=$dbh->prepare("drop user if exists $x2goadmin");
	$sth->execute();
	#create db
	$sth=$dbh->prepare("create database $db");
	$sth->execute() or die;
	#create x2goadmin
	$sth=$dbh->prepare("create USER $x2goadmin WITH ENCRYPTED PASSWORD '$x2goadminpass'");
	$sth->execute() or die;
	#save x2goadmin password
	open (FL,"> /etc/x2go/x2gosql/passwords/x2gopgadmin") or die "Can't write password file /etc/x2go/x2gosql/passwords/x2gopgadmin";
	print FL $x2goadminpass;
	close(FL);
	$sth->finish();
	undef $dbh;
}

sub mysql_create_database
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";
	my $db = shift or die "No db parameter supplied";
	my $x2goadmin = shift or die "No x2goadmin (user-level database username) parameter supplied";
	my $x2goadminpass = shift or die "No x2goadminpass (user-level database pasword) parameter supplied";
	my $unet = shift or die "No unet parameter supplied";

	my $dbh=DBI->connect("dbi:mysql:database=mysql;host=$host;port=$port", "$dbadmin", "$dbadminpass", {AutoCommit => 1}) or die $_;
	#drop db if exists
	my $sth=$dbh->prepare("DROP DATABASE IF EXISTS `$db`");
	$sth->execute();
	#drop x2goadmin
	$sth=$dbh->prepare("DROP USER IF EXISTS `$x2goadmin`@`$unet`");
	$sth->execute();
	#create db
	$sth=$dbh->prepare("CREATE DATABASE IF NOT EXISTS `$db`");
	$sth->execute() or die;
	#create x2goadmin
	$sth=$dbh->prepare("CREATE USER `$x2goadmin`@`$unet` IDENTIFIED BY '$x2goadminpass'");
	$sth->execute() or die;
	#save x2goadmin password
	open (FL,"> /etc/x2go/x2gosql/passwords/x2gomysqladmin") or die "Can't write password file /etc/x2go/x2gosql/passwords/x2gomysqladmin";
	print FL $x2goadminpass;
	close(FL);
	$sth->finish();
	undef $dbh;
}

sub list_users
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";

	my $sslmode = undef;

	if ($Config->param("backend") eq 'postgres')
	{
		$sslmode = shift or die "No sslmode parameter supplied";

		pg_list_users($host, $port, $dbadmin, $dbadminpass, $sslmode);
	}
	elsif ($Config->param("backend") eq 'mysql')
	{
		mysql_list_users($host, $port, $dbadmin, $dbadminpass);
	}
	else
	{
		die "Invalid database backend";
	}
}

sub pg_list_users
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";
	my $sslmode = shift or die "No sslmode parameter supplied";

	my $dbh=DBI->connect("dbi:Pg:dbname=postgres;host=$host;port=$port;sslmode=$sslmode", "$dbadmin", "$dbadminpass", {AutoCommit => 1}) or die $_;
	my $sth=$dbh->prepare("select rolname from pg_roles where rolname like 'x2gouser_%'");
	$sth->execute()or die;
	printf ("%-20s DB user\n","UNIX user");
	print "---------------------------------------\n";
	my @data;
	while (@data = $sth->fetchrow_array)
	{
		@data[0]=~s/x2gouser_//;
		printf ("%-20s x2gouser_@data[0]\n",@data[0]);
	}
	$sth->finish();
	undef $dbh;
}

sub mysql_list_users
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";

	my $dbh=DBI->connect("dbi:mysql:host=$host;port=$port", "$dbadmin", "$dbadminpass", {AutoCommit => 1}) or die $_;
	my $sth=$dbh->prepare("select user from mysql.user where user like 'x2gouser_%'");
	$sth->execute()or die;
	printf ("%-20s DB user\n","UNIX user");
	print "---------------------------------------\n";
	my @data;
	while (@data = $sth->fetchrow_array)
	{
		@data[0]=~s/x2gouser_//;
		printf ("%-20s x2gouser_@data[0]\n",@data[0]);
	}
	$sth->finish();
	undef $dbh;
}

sub add_user
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";
	my $db = shift or die "No db parameter supplied";
	my $user_to_add = shift or die "No user-to-add parameter supplied";

	my $sslmode = undef;
	my $unet = undef;

	if ($Config->param("backend") eq 'postgres')
	{
		$sslmode = shift or die "No sslmode parameter supplied";

		pg_add_user($host, $port, $dbadmin, $dbadminpass, $db, $user_to_add, $sslmode);
	}
	elsif ($Config->param("backend") eq 'mysql')
	{
		$unet = shift or die "No unet parameter supplied";

		mysql_add_user($host, $port, $dbadmin, $dbadminpass, $db, $user_to_add, $unet);
	}
	else
	{
		die "Invalid database backend";
	}
}

sub pg_add_user
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";
	my $db = shift or die "No db parameter supplied";
	my $user_to_add = shift or die "No user-to-add parameter supplied";
	my $sslmode = shift or die "No sslmode parameter supplied";

	my $dbh=DBI->connect("dbi:Pg:dbname=$db;host=$host;port=$port;sslmode=$sslmode", "$dbadmin", "$dbadminpass", {AutoCommit => 1}) or die $_;
	my ($name, $pass, $uid, $pgid, $quota, $comment, $gcos, $dir, $shell, $expire) = getpwnam($user_to_add);
	if (! $name)
	{
		print "Cannot find user ($user_to_add)\n";
		return;
	}
	elsif ($name eq "root")
	{
		print "The super-user \"root\" is not allowed to use X2Go\n";
		return;
	}
	$pass=`pwgen -s -c -n 32 1`;
	chomp($pass);

	my $sth=$dbh->prepare("REVOKE ALL PRIVILEGES ON sessions, used_ports, mounts FROM \"x2gouser_$user_to_add\"");
	$sth->{Warn}=0;
	$sth->{PrintError}=0;
	$sth->execute();

	my $sth=$dbh->prepare("REVOKE ALL PRIVILEGES ON sessions_view, mounts_view, servers_view, ports_view FROM \"x2gouser_$user_to_add\"");
	$sth->{Warn}=0;
	$sth->{PrintError}=0;
	$sth->execute();

	my $sth=$dbh->prepare("DROP OWNED BY \"x2gouser_$user_to_add\"");
	$sth->{Warn}=0;
	$sth->{PrintError}=0;
	$sth->execute();

	$sth=$dbh->prepare("drop USER if exists \"x2gouser_$user_to_add\"");
	$sth->{Warn}=0;
	$sth->{PrintError}=0;
	$sth->execute();

	print ("create DB user \"x2gouser_$user_to_add\"\n");
	$sth=$dbh->prepare("create USER \"x2gouser_$user_to_add\" WITH ENCRYPTED PASSWORD '$pass'");
	$sth->execute();

	$sth=$dbh->prepare("GRANT INSERT, UPDATE, DELETE ON sessions, used_ports, mounts TO \"x2gouser_$user_to_add\"");
	$sth->execute();

	$sth=$dbh->prepare("GRANT SELECT ON used_ports TO \"x2gouser_$user_to_add\"");
	$sth->execute();

	$sth=$dbh->prepare("GRANT SELECT, UPDATE, DELETE ON sessions_view, mounts_view, servers_view, ports_view TO \"x2gouser_$user_to_add\"");
	$sth->execute();
	$sth->finish();

	if (! -d "$dir/.x2go" )
	{
			if ( defined (&File::Path::make_path) )
			{
				File::Path::make_path("$dir/.x2go");
			}
			elsif ( defined (&File::Path::mkpath) )
			{
				File::Path::mkpath("$dir/.x2go");
			}
			else
			{
				die "Unable to create folders with File::Path";
			}
	}

	#save user password
	open (FL,"> $dir/.x2go/pgsqlpass") or die "Can't open password file $dir/.x2go/pgsqlpass";
	print FL $pass;
	close(FL);
	chmod(0700,"$dir/.x2go");
	chown($uid,$pgid,"$dir/.x2go");
	chmod(0600,"$dir/.x2go/pgsqlpass");
	chown($uid,$pgid,"$dir/.x2go/pgsqlpass");
}

sub mysql_add_user
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";
	my $db = shift or die "No db parameter supplied";
	my $user_to_add = shift or die "No user-to-add parameter supplied";
	my $unet = shift or die "No unet parameter supplied";

	my $dbh=DBI->connect("dbi:mysql:database=$db;host=$host;port=$port", "$dbadmin", "$dbadminpass", {AutoCommit => 1}) or die $_;
	my ($name, $pass, $uid, $pgid, $quota, $comment, $gcos, $dir, $shell, $expire) = getpwnam($user_to_add);
	if (! $name)
	{
		print "Cannot find user ($user_to_add)\n";
		return;
	}
	elsif ($name eq "root")
	{
		print "The super-user \"root\" is not allowed to use X2Go\n";
		return;
	}
	$pass=`pwgen -s -c -n 32 1`;
	chomp($pass);

	my $sth=$dbh->prepare("DROP USER IF EXISTS `x2gouser_$user_to_add`@`$unet`");
	$sth->{Warn}=0;
	$sth->{PrintError}=0;
	$sth->execute();

	print ("create DB user \"x2gouser_$user_to_add\"\n");
	$sth=$dbh->prepare("CREATE USER `x2gouser_$user_to_add`@`$unet` IDENTIFIED BY '$pass'");
	$sth->execute();

	$sth=$dbh->prepare("GRANT SELECT, INSERT, UPDATE, DELETE ON * TO `x2gouser_$user_to_add`@`$unet`");
	$sth->execute();

	$sth->finish();

	if (! -d "$dir/.x2go" )
	{
			if ( defined (&File::Path::make_path) )
			{
				File::Path::make_path("$dir/.x2go");
			}
			elsif ( defined (&File::Path::mkpath) )
			{
				File::Path::mkpath("$dir/.x2go");
			}
			else
			{
				die "Unable to create folders with File::Path";
			}
	}

	#save user password
	open (FL,"> $dir/.x2go/mysqlpass") or die "Can't open password file $dir/.x2go/mysqlpass";
	print FL $pass;
	close(FL);
	chmod(0700,"$dir/.x2go");
	chown($uid,$pgid,"$dir/.x2go");
	chmod(0600,"$dir/.x2go/mysqlpass");
	chown($uid,$pgid,"$dir/.x2go/mysqlpass");
}

sub rm_user
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";
	my $db = shift or die "No db parameter supplied";
	my $user_to_remove = shift or die "No user-to-remove parameter supplied";

	my $sslmode = undef;
	my $unet = undef;

	if ($Config->param("backend") eq 'postgres')
	{
		$sslmode = shift or die "No sslmode parameter supplied";

		pg_rm_user($host, $port, $dbadmin, $dbadminpass, $db, $user_to_remove, $sslmode);
	}
	elsif ($Config->param("backend") eq 'mysql')
	{
		$unet = shift or die "No unet parameter supplied";

		mysql_rm_user($host, $port, $dbadmin, $dbadminpass, $db, $user_to_remove, $unet);
	}
	else
	{
		die "Invalid database backend";
	}
}

sub pg_rm_user
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";
	my $db = shift or die "No db parameter supplied";
	my $user_to_remove = shift or die "No user-to-remove parameter supplied";
	my $sslmode = shift or die "No sslmode parameter supplied";

	my $dbh=DBI->connect("dbi:Pg:dbname=$db;host=$host;port=$port;sslmode=$sslmode", "$dbadmin", "$dbadminpass", {AutoCommit => 1}) or die $_;

	print ("rm DB user \"x2gouser_$user_to_remove\"\n");

	my $sth=$dbh->prepare("REVOKE ALL PRIVILEGES ON sessions, used_ports, mounts FROM \"x2gouser_$user_to_remove\"");
	$sth->execute();

	my $sth=$dbh->prepare("REVOKE ALL PRIVILEGES ON sessions_view, mounts_view, servers_view, ports_view FROM \"x2gouser_$user_to_remove\"");
	$sth->execute();

	my $sth=$dbh->prepare("DROP OWNED BY \"x2gouser_$user_to_remove\"");
	$sth->execute();

	my $sth=$dbh->prepare("drop USER if exists \"x2gouser_$user_to_remove\"");
	$sth->execute();
	$sth->finish();

	my ($name, $pass, $uid, $pgid, $quota, $comment, $gcos, $dir, $shell, $expire) = getpwnam($user_to_remove);
	if (! $uid)
	{
		return;
	}
	if ( -e "$dir/.x2go/pgsqlpass" )
	{
		unlink("$dir/.x2go/pgsqlpass");
	}
	if ( -e "$dir/.x2go/sqlpass" )
	{
		unlink("$dir/.x2go/sqlpass");
	}
}

sub mysql_rm_user
{
	my $host = shift or die "No host parameter supplied";
	my $port = shift or die "No port parameter supplied";
	my $dbadmin = shift or die "No dbadmin parameter supplied";
	my $dbadminpass = shift or die "No dbadminpass (database administrator password) parameter supplied";
	my $db = shift or die "No db parameter supplied";
	my $user_to_remove = shift or die "No user-to-remove parameter supplied";
	my $unet = shift or die "No unet parameter supplied";

	my $dbh=DBI->connect("dbi:mysql:database=$db;host=$host;port=$port", "$dbadmin", "$dbadminpass", {AutoCommit => 1}) or die $_;

	print ("rm DB user \"x2gouser_$user_to_remove\"\n");

	my $sth=$dbh->prepare("DROP USER IF EXISTS `x2gouser_$user_to_remove`@`$unet`");
	$sth->execute();
	$sth->finish();

	my ($name, $pass, $uid, $pgid, $quota, $comment, $gcos, $dir, $shell, $expire) = getpwnam($user_to_remove);
	if (! $uid)
	{
		return;
	}
	if ( -e "$dir/.x2go/mysqlpass" )
	{
		unlink("$dir/.x2go/mysqlpass");
	}
}
