//
// C++ Implementation: sharetray
//
// Description:
//
//
// Author: Oleksandr Shneyder <o.shneyder@phoca-gmbh.de>, (C) 2006-2018
//         Mike Gabriel <mike.gabriel@das-netzwerkteam.de>, (C) 2011-2018
//
// Copyright: See COPYING file that comes with this distribution
//
//
#include "sharetray.h"
#include <QMenu>
#include <QApplication>
#include <QLocalServer>
#include <QLocalSocket>
#include <QDir>
#include <QtDebug>
#include <QMessageBox>
#include <QDialog>
#include <QSocketNotifier>
#include <sys/socket.h>
#include <sys/unistd.h>
#include "simplelocalsocket.h"
#include "accessaction.h"
#include "accessdialog.h"
#include <sys/types.h>
#include <csignal>
#include <errno.h>
#include <QToolTip>
#include <QTimer>
#include <QSettings>
#include <QSystemTrayIcon>
#include <QCloseEvent>
#include <QDateTime>
#include <grp.h>
#include <QProcess>
#include <QFileInfo>
#include <stdint.h>
#define STAT_ACT_COUNT 10

#define VERSION "3.2.0.0"

//needed to not get an undefined reference to static members
int ShareTray::sigkeybintFd[2];
int ShareTray::sigtermFd[2];
int ShareTray::sigabortFd[2];
int ShareTray::sighupFd[2];

bool fileExists(QString path) {
	QFileInfo check_file(path);
	// check if file exists and if yes: Is it really a file and no directory?
	if (check_file.exists() && check_file.isFile()) {
		return true;
	} else {
		return false;
	}
}

ShareTray::ShareTray()
        : QMainWindow()
{
	serverSocket=0l;
	menuClose=false;
	current_list=BLACK;

	/* only hard-coded default... */
	sharingGroup= "x2godesktopsharing";

	ui.setupUi ( this );
	ui.box->setSelectionMode ( QAbstractItemView::ExtendedSelection );

	connect ( ui.close_box,SIGNAL ( clicked ( QAbstractButton* ) ),
	          SLOT ( slotMsgClose ( QAbstractButton* ) ) );

	connect ( ui.ok_cancel_box,SIGNAL ( clicked ( QAbstractButton* ) ),
	          SLOT ( slotMsgOkCancel ( QAbstractButton* ) ) );

	connect ( ui.del,SIGNAL ( clicked() ),
	          SLOT ( slotDelListItem() ) );

	ui.icon->setPixmap ( QPixmap ( ":icons/128x128/x2godesktopsharing.png" ) );

	ui.text->setText ( tr ( "<b>X2Go Desktop Sharing v" ) +VERSION+
	                   " </b >(Qt - "+qVersion() +")"+
	                   ui.text->text() );

	setWindowFlags ( Qt::Dialog );
	Qt::WindowFlags flags=windowFlags();
	flags&= ~Qt::WindowTitleHint;
	setWindowFlags ( flags );

	QString dispname=getenv ( "DISPLAY" );
	// socketFname=QDir::tempPath() +"x2godesktopsharing_@"+
	socketFname = "/tmp/x2godesktopsharing_@";
	socketFname += getenv ( "LOGNAME" );
	socketFname += "@" + dispname;
	// lockFname=QDir::tempPath() +"x2godesktopsharing.lock_"+
	lockFname = "/tmp/x2godesktopsharing.lock_";
	lockFname += getenv ( "LOGNAME" );
	lockFname += "@" + dispname;
	if ( QFile::exists ( lockFname ) )
	{
		QFile file ( lockFname );
		if ( file.open ( QIODevice::ReadOnly | QIODevice::Text ) )
		{
			QTextStream in ( &file );
			if ( !in.atEnd() )
			{
				QString line = in.readLine();
				file.close();
				uint32_t fetch_time = line.toUInt ();
				uint32_t cur_time = QDateTime::currentDateTime ().toTime_t ();
				int64_t time_diff = static_cast<int64_t> (fetch_time) - static_cast<int64_t> (cur_time);
				if (abs (time_diff) < 5) {
					QString message=QString (
					                tr (
					                    "X2Go desktop sharing application "
					                    "is already active for this "
					                    "display \n"
					                    "if this application is no longer "
					                    "running, remove %1\n"
					                    "and start again" ) ).arg ( lockFname );
					QMessageBox::critical ( 0l,tr (
					                                "Error" ),message,
					                                QMessageBox::Ok,
					                                QMessageBox::NoButton );
					exit ( -1 );
				}
			}
		}
		QFile::remove ( lockFname );
	}
	if ( QFile::exists ( socketFname ) )
		QFile::remove ( socketFname );

	QTimer *lockTimer = new QTimer ( this );
	connect ( lockTimer, SIGNAL ( timeout() ), this, SLOT ( slotUpdateLockFile() ) );
	lockTimer->start ( 3000 );
	trayIcon=new QSystemTrayIcon ( this );
	setWindowIcon ( QIcon ( ":icons/22x22/x2godesktopsharing.png" ) );
	menu = new QMenu ( this );
	trayIcon->setContextMenu ( menu );
	trayIcon->setToolTip ( tr ( "X2Go desktop sharing application" ) );

	menu->addSeparator();

	actWhite = menu->addAction ( QIcon ( ":icons/32x32/wlist.png" ) ,
	                             tr ( "Granted users..." ) );
	actBlack = menu->addAction ( QIcon ( ":icons/32x32/blist.png" ) ,
	                             tr ( "Banned users..." ) );
	menu->addSeparator();

	actStart = menu->addAction ( QIcon ( ":icons/32x32/share.png" ) ,
	                             tr ( "Activate desktop sharing" ) );

	actStop = menu->addAction ( QIcon ( ":icons/32x32/stop.png" ) ,
	                            tr ( "Deactivate desktop sharing" ) );

	menu->addSeparator();
	QAction* actAbout=menu->addAction (
	                                    QIcon ( ":icons/32x32/x2godesktopsharing.png" ),
	                                    tr ( "About X2Go Desktop Sharing" ) );
	//
	//     QAction* actAboutQt=menu->addAction(
	//                              tr("About Qt"));
	//     connect ( actAboutQt,SIGNAL ( triggered ( bool ) ),this,
	//          SLOT ( slotAboutQt() ) );
	//
	connect ( actAbout,SIGNAL ( triggered ( bool ) ),this,
	          SLOT ( slotAbout() ) );


	menu->addSeparator();
	QAction* actExit = menu->addAction ( QIcon ( ":icons/32x32/exit.png" ) ,
	                                     tr ( "&Quit" ) );

	connect ( actWhite,SIGNAL ( triggered ( bool ) ),this,
	          SLOT ( slotWhiteList() ) );
	connect ( actBlack,SIGNAL ( triggered ( bool ) ),this,
	          SLOT ( slotBlackList() ) );

	connect ( actExit,SIGNAL ( triggered ( bool ) ),this,
	          SLOT ( slotMenuClose() ) );

	connect ( actStart,SIGNAL ( triggered ( bool ) ),this,
	          SLOT ( slotStartSharing() ) );

	connect ( actStop,SIGNAL ( triggered ( bool ) ),this,
	          SLOT ( slotStopSharing() ) );

	actStop->setEnabled ( false );

	// unix signals (TERM, INT) are piped into a unix socket and will raise Qt events
	if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sigkeybintFd))
		qFatal("Couldn't create keyboard INT socketpair");

	if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sigtermFd))
		qFatal("Couldn't create TERM socketpair");

	if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sigabortFd))
		qFatal("Couldn't create ABRT socketpair");

	if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sighupFd))
		qFatal("Couldn't create HANGUP socketpair");

	snKeybInt = new QSocketNotifier(sigkeybintFd[1], QSocketNotifier::Read, this);
	connect(snKeybInt, SIGNAL(activated(int)), this, SLOT(handleSigKeybInt()));
	snTerm = new QSocketNotifier(sigtermFd[1], QSocketNotifier::Read, this);
	connect(snTerm, SIGNAL(activated(int)), this, SLOT(handleSigTerm()));
	snAbort = new QSocketNotifier(sigabortFd[1], QSocketNotifier::Read, this);
	connect(snAbort, SIGNAL(activated(int)), this, SLOT(handleSigAbort()));
	snHup = new QSocketNotifier(sighupFd[1], QSocketNotifier::Read, this);
	connect(snHup, SIGNAL(activated(int)), this, SLOT(handleSigHup()));

	QTimer *timer = new QTimer ( this );
	connect ( timer, SIGNAL ( timeout() ), this, SLOT ( slotTimer() ) );
	timer->start ( 5000 );

	QStringList args=QCoreApplication::arguments();
	for ( int i=1; i<args.size(); ++i )
	{
		if ( !parseParameter ( args[i] ) )
		{
			close();
			exit (-1);
		}
	}

	loadSystemSettings();
	loadUserSettings();
	setTrayIcon();
	trayIcon->show();
}


ShareTray::~ShareTray()
{
	qDebug() <<"stopping desktop sharing";
	slotStopSharing();
	if ( QFile::exists ( lockFname ) )
	{
		QFile::remove ( lockFname );
		qDebug() <<"lock file removed";
	}
	saveUserSettings();
	qDebug() <<"settings saved";
}


bool ShareTray::parseParameter( QString param )
{
	if ( param == "--activate-desktop-sharing" )
	{
		slotStartSharing();
		return true;
	}
	return false;
}


void ShareTray::handleSigKeybInt()
{
	snKeybInt->setEnabled(false);
	char tmp;
	::read(sigkeybintFd[1], &tmp, sizeof(tmp));

	// do Qt stuff here
	slotMenuClose();

	snKeybInt->setEnabled(true);
}

void ShareTray::handleSigTerm()
{
	snTerm->setEnabled(false);
	char tmp;
	::read(sigtermFd[1], &tmp, sizeof(tmp));

	// do Qt stuff here
	slotMenuClose();

	snTerm->setEnabled(true);
}

void ShareTray::handleSigAbort()
{
	snAbort->setEnabled(false);
	char tmp;
	::read(sigabortFd[1], &tmp, sizeof(tmp));

	// do Qt stuff here
	slotMenuClose();

	snAbort->setEnabled(true);
}

void ShareTray::handleSigHup()
{
	snHup->setEnabled(false);
	char tmp;
	::read(sighupFd[1], &tmp, sizeof(tmp));

	// do Qt stuff here
	slotMenuClose();

	snHup->setEnabled(true);
}

void ShareTray::keybintSignalHandler(int)
{
	char a = 1;
	::write(sigkeybintFd[0], &a, sizeof(a));
}

void ShareTray::termSignalHandler(int)
{
	char a = 1;
	::write(sigtermFd[0], &a, sizeof(a));
}

void ShareTray::abortSignalHandler(int)
{
	char a = 1;
	::write(sigabortFd[0], &a, sizeof(a));
}

void ShareTray::hupSignalHandler(int)
{
	char a = 1;
	::write(sighupFd[0], &a, sizeof(a));
}

void ShareTray::slotStopSharing()
{
	if ( serverSocket )
	{
		serverSocket->close();
		delete serverSocket;
		serverSocket=0l;
	}
	if ( QFile::exists ( socketFname ) )
		QFile::remove ( socketFname );
	for ( int i=menu->actions().count()-STAT_ACT_COUNT-1;i>=0;--i )
	{
		slotCloseConnection ( ( AccessAction* ) ( menu->actions() [i] ) );
	}
	actStop->setEnabled ( false );
	actStart->setEnabled ( true );
	setTrayIcon();
}


void ShareTray::slotStartSharing()
{
	actStop->setEnabled ( true );
	actStart->setEnabled ( false );
	if ( serverSocket )
		delete serverSocket;
	if ( QFile::exists ( socketFname ) )
		QFile::remove ( socketFname );
	serverSocket=new QLocalServer ( this );
	if ( serverSocket->listen ( socketFname ) )
	{

		chown ( socketFname.toLatin1(),getuid(),getgrnam ( sharingGroup.toLatin1() )->gr_gid );
		QFile::setPermissions ( socketFname,
		                        QFile::ReadOwner|QFile::WriteOwner|QFile::ReadGroup|QFile::WriteGroup );
		connect ( serverSocket,SIGNAL ( newConnection() ),
		          this,SLOT ( slotServerConnection() ) );
	}
	else
	{
		QString message=
		    tr (
		        "Can't listen on socket:" ) + socketFname;
		QMessageBox::critical ( 0l,tr (
		                            "Error" ),message,
		                        QMessageBox::Ok,
		                        QMessageBox::NoButton );
		close();
	}
	setTrayIcon();
}

bool ShareTray::acceptConnections()
{
	return actStop->isEnabled();
}

QString ShareTray::getSocketAnswer ( QString message )
{
	qDebug() <<"message: "<<message;
	QStringList lst=message.split ( ' ' );
	if ( lst.size() !=11 )
	{
		qDebug() <<"wrong parameters";
		return "DENY wrong number of session parameters";
	}
	QStringList params=lst[9].split ( "XSHAD" );
	if ( params.size() !=3 )
	{
		qDebug() <<"wrong parameters";
		return "DENY wrong session name";
	}
	QString client=lst[0];
	QString user=params[1];
	QString remote_user=lst[10];
	if ( getAccess ( remote_user, client ) ==QDialog::Accepted )
	{
		trayMessage ( tr ( "Access granted" ),QString ( tr ( "User \"%1\" ([%2]): Access granted." ) ).arg (remote_user ).arg ( client ) );
		//start agent
		QProcess proc ( this );
		lst.removeAt ( 0 );;
		QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
		env.insert("X2GO_CLIENT", client);
		proc.setProcessEnvironment(env);
		proc.start ( "x2gostartagent",lst );
		if ( !proc.waitForFinished ( 20000 ) )
		{
			return "DENY x2gostartagent timed out after 20s";
		}
		else
		{
			QString output=proc.readAllStandardOutput();
			qDebug() <<"agent out: "<<output;
			QStringList lines=output.split ( "\n" );
			QString pid="pid";
			if ( lines.size() >3 )
				pid=lines[2];
			qDebug() <<"agent pid: "<<pid;
			AccessAction *act=new AccessAction (
			    pid,remote_user,client,
			    QString ( tr ( "Disconnect \"%1\" (on host [%2])" ) ).arg ( remote_user ).arg ( client ),
			    this );
			menu->insertAction ( menu->actions() [0],act );
			connect ( act,SIGNAL ( actionActivated ( AccessAction* ) ),this,
			          SLOT ( slotCloseConnection ( AccessAction* ) ) );
			trayMessage ( tr ( "Remote connection" ),
			              QString (
			                  tr ( "User \"%1\" ([%2]) is now connected." ) ).arg ( remote_user ).arg ( client ) );
			setTrayIcon();
			return output;
		}
	}
	trayMessage ( tr ( "Access denied" ),QString (
	              tr ( "User \"%1\" ([%2]): Access denied." ) ).arg ( remote_user ).arg ( client ) );
	return "DENY access denied user";
}

void ShareTray::closeSocket ( SimpleLocalSocket* sock )
{
	qDebug() <<"closing null socket";
	if ( sock )
	{
		qDebug() <<"closing socket";
		delete sock;
		qDebug() <<"done";
	}
}

void ShareTray::slotServerConnection()
{
	new SimpleLocalSocket ( this,serverSocket->nextPendingConnection() );
}

void ShareTray::slotCloseConnection ( AccessAction* action )
{
	kill ( action->pid().toUInt(),15 );

	trayMessage ( tr ( "User has been disconnected" ),
	              QString (
	                  tr ( "User \"%1\" ([%2]) is now disconnected." ) ).arg (
	                  action->user() ).arg ( action->host() ) );

	menu->removeAction ( action );
	delete action;
	setTrayIcon();
}


int ShareTray::getAccess ( QString remote_user, QString host )
{

	// user preferences always supercede system-wide preferences
	if ( userWhiteList.contains ( remote_user ) )
		return QDialog::Accepted;
	if ( userBlackList.contains ( remote_user ) )
		return QDialog::Rejected;

	// system-wide preferences act as defaults for all users, but can be overriden by the user...
	if ( systemwideWhiteList.contains ( remote_user ) )
		return QDialog::Accepted;
	if ( systemwideBlackList.contains ( remote_user ) )
		return QDialog::Rejected;

	AccessWindow ad ( remote_user, host, this );
	ad.raise();
	int res=ad.exec();
	if ( ad.isChecked() &&res==QDialog::Accepted )
		userWhiteList<<remote_user;
	if ( ad.isChecked() &&res==QDialog::Rejected )
		userBlackList<<remote_user;
	actBlack->setEnabled ( userBlackList.size() >0 );
	actWhite->setEnabled ( userWhiteList.size() >0 );
	return res;
}

void ShareTray::closeEvent ( QCloseEvent*  ev )
{
	if ( !menuClose )
	{
		ev->ignore();
		hide();
		return;
	}
	qDebug() <<"stopping desktop sharing";
	slotStopSharing();
	if ( QFile::exists ( lockFname ) )
	{
		QFile::remove ( lockFname );
		qDebug() <<"lock file removed";
	}
	saveUserSettings();
	qDebug() <<"settings saved";
}

void ShareTray::slotTimer()
{
	for ( int i=menu->actions().count()-STAT_ACT_COUNT-1;i>=0;--i )
	{
		AccessAction* action= ( AccessAction* ) ( menu->actions() [i] );
		if ( !isProcessRunning ( action->pid() ) )
		{
			trayMessage ( tr ( "User disconnected" ),
			              QString (
			                  tr ( "User \"%1\" ([%2]) disconnected" ) ).arg (
			                  action->user() ).arg ( action->host() ) );
			menu->removeAction ( action );
			delete action;
		}
	}
	setTrayIcon();
}

void ShareTray::trayMessage ( QString title, QString text )
{
	if ( !QSystemTrayIcon::supportsMessages () )
		QToolTip::showText ( geometry().topLeft(),
		                     text );
	else
		trayIcon->showMessage ( title,text );
}


bool ShareTray::isProcessRunning ( QString pid )
{
	if ( kill ( pid.toInt(),SIGCONT ) ==-1 )
	{
		if ( errno==ESRCH )
		{
			return false;
		}
	}
	return true;
}


void ShareTray::slotBlackList()
{
	current_list=BLACK;
	showList();
	setWindowTitle ( tr ( "Banned users" ) );
}

void ShareTray::slotWhiteList()
{
	current_list=WHITE;
	showList();
	setWindowTitle ( tr ( "Granted users" ) );
}

void ShareTray::showList()
{
	show();
	ui.ok_cancel_box->show();
	ui.box->show();
	ui.del->show();

	ui.close_box->hide();
	ui.icon->hide();
	ui.text->hide();

	QStringList* lst;
	if ( current_list==BLACK )
		lst=&userBlackList;
	else
		lst=&userWhiteList;
	lst->sort();
	ui.box->clear();
	ui.box->insertItems ( 0,*lst );
}

void ShareTray::loadSystemSettings()
{
	QString settings_file = "/etc/x2godesktopsharing/settings";
	if ( fileExists (settings_file) ) {

		qDebug() << "loading system-wide settings: "<<settings_file;

		QSettings st ( settings_file, QSettings::NativeFormat );

		systemwideBlackList= st.value ( "blacklist" ).toStringList();
		systemwideWhiteList= st.value ( "whitelist" ).toStringList();

		if ( ( systemwideBlackList.length() > 0 ) && ( systemwideBlackList[0] == "") ) systemwideBlackList.removeFirst();
		if ( ( systemwideWhiteList.length() > 0 ) && ( systemwideWhiteList[0] == "") ) systemwideWhiteList.removeFirst();

		/* the system-wide settings will be loaded on first usage and
		 * copied into the user settings' "group" parameter and further-on
		 * loaded from there...
		 */
		sharingGroup= (QString) st.value ( "group" ).toString();

		if (sharingGroup != "") {

			qDebug() << "system-wide desktop sharing POSIX group is: "<<sharingGroup;

		}
		else {

			qDebug() << "using hard-coded default for desktop sharing POSIX group is: " << sharingGroup;

		}

		actBlack->setEnabled ( systemwideBlackList.size() >0 );
		actWhite->setEnabled ( systemwideWhiteList.size() >0 );

	}
}

void ShareTray::loadUserSettings()
{
	QString userSharingGroup;

	QString settings_file = QDir::homePath() +"/.x2godesktopsharing/settings";
	if ( fileExists ( settings_file ) ) {

		qDebug() << "loading user settings: "<<settings_file;

		QSettings st ( settings_file, QSettings::NativeFormat );

		userBlackList= st.value ( "blacklist" ).toStringList();
		userWhiteList= st.value ( "whitelist" ).toStringList();

		if ( ( userBlackList.length() > 0 ) && ( userBlackList[0] == "") ) userBlackList.removeFirst();
		if ( ( userWhiteList.length() > 0 ) && ( userWhiteList[0] == "") ) userWhiteList.removeFirst();

		/* after first usage of x2godesktopsharing, the system-wide settings
		 * get ignored, as they are always loaded
		 */
		userSharingGroup= st.value ( "group" ).toString();
		if (userSharingGroup != "") {

			qDebug() << "user-defined desktop sharing POSIX group is: "<<sharingGroup<<"; using this!";
			sharingGroup=userSharingGroup;
		}
		else {
			qDebug() << "user-defined desktop sharing POSIX group unset, using system-wide configured group: "<<sharingGroup;
		}

		actBlack->setEnabled ( userBlackList.size() >0 );
		actWhite->setEnabled ( userWhiteList.size() >0 );

	}
}

void ShareTray::saveUserSettings()
{
	QSettings st ( QDir::homePath() +"/.x2godesktopsharing/settings",
	               QSettings::NativeFormat );

	st.setValue ( "group",sharingGroup );
	if (userBlackList.length() > 0)
		st.setValue ( "blacklist",userBlackList );
	else
		st.setValue ( "blacklist", "" );

	if (userWhiteList.length() > 0)
		st.setValue ( "whitelist",userWhiteList );
	else
		st.setValue ( "whitelist", "" );
}

void ShareTray::setTrayIcon()
{
	if ( !acceptConnections() )
	{
		trayIcon->setIcon ( QIcon ( ":icons/22x22/discard.png" ) );
		return;
	}
	if ( menu->actions().count() >STAT_ACT_COUNT )
	{
		trayIcon->setIcon ( QIcon ( ":icons/22x22/view.png" ) );
		return;
	}
	trayIcon->setIcon ( QIcon ( ":icons/22x22/accept.png" ) );
}



void ShareTray::slotAbout()
{
	setWindowTitle ( tr ( "X2Go Desktop Sharing" ) );

	show();
	ui.ok_cancel_box->hide();
	ui.box->hide();
	ui.del->hide();

	ui.close_box->show();
	ui.icon->show();
	ui.text->show();
}


void ShareTray::slotAboutQt()
{
	QMessageBox::aboutQt ( 0 );
}

void ShareTray::slotMsgOkCancel ( QAbstractButton* button )
{
	if ( ui.ok_cancel_box->buttonRole ( button ) ==QDialogButtonBox::AcceptRole )
	{
		QStringList* lst;
		if ( current_list==BLACK )
			lst=&userBlackList;
		else
			lst=&userWhiteList;
		lst->clear();
		for ( int i=ui.box->count()-1;i>=0;--i )
		{
			*lst<<ui.box->item ( i )->text();
		}
	}
	actBlack->setEnabled ( userBlackList.size() >0 );
	actWhite->setEnabled ( userWhiteList.size() >0 );
	hide();
}

void ShareTray::slotMsgClose ( QAbstractButton* )
{
	hide();
}

void ShareTray::slotDelListItem()
{
	for ( int i=ui.box->count()-1;i>=0;--i )
	{
		QListWidgetItem* it=ui.box->item ( i );
		if ( it->isSelected() )
		{
			ui.box->takeItem ( i );
			delete it;
		}
	}
}

void ShareTray::slotMenuClose()
{
	menuClose=true;
	close();
}

void ShareTray::slotUpdateLockFile()
{
	QFile file ( lockFname );
	if ( file.open ( QIODevice::WriteOnly | QIODevice::Text ) )
	{
		QTextStream out ( &file );
		out<<QDateTime::currentDateTime().toTime_t();
	}
}
