#!/usr/bin/perl

# X2Go CUPS backend
# Copyright (C) 2009-2018 X2Go Project - https://wiki.x2go.org
# Copyright (C) 2009-2018 Obviously Nice
#
#  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
#  Free Software Foundation, Inc.,
#  51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

use Sys::Syslog qw( :standard :macros );
use Sys::Hostname;
use File::Basename;
use File::Copy;
use File::Temp qw( tempfile );
use IPC::Open2;
use Text::ParseWords;
use Errno qw( EINTR EIO :POSIX );
use strict;

openlog($0,'cons,pid','user');

## if the CUPS server is X2Go server at the same time, use ,,local'' here, otherwise
## name the hostname of your master X2Go server within in your X2Go cluster. This
## host then is used for querying session information.
my $x2goserver = "local";

## DSA key for user x2goprint (new path and filename, default in cups-x2go backend)
## this private key has to be valid for all X2Go servers that print via the CUPS
## server that cups-x2go is installed on. The corresponding public key has to
## be installed in <remote-x2goserverX>:~x2goprint/.ssh/authorized_keys
my $printdsa = "/root/.ssh/id_dsa-x2goprint";

# PS2PDF command
my $ps2pdf = "/usr/bin/gs -q -dCompatibilityLevel=1.4 -dNOPAUSE -dBATCH -dSAFER -sDEVICE=pdfwrite -sOutputFile=- -dAutoRotatePages=/PageByPage -dAutoFilterColorImages=false -dColorImageFilter=/FlateEncode -dPDFSETTINGS=/prepress -dDoNumCopies -c .setpdfwrite -f \"%s\"";
#my $ps2pdf = "/usr/bin/gs -q -dCompatibilityLevel=1.4 -dNOPAUSE -dBATCH -dSAFER -sDEVICE=pdfwrite -sOutputFile=- -dAutoRotatePages=/PageByPage -dAutoFilterColorImages=false -dColorImageFilter=/FlateEncode -dPDFSETTINGS=/prepress -dDoNumCopies -c .setpdfwrite -f /usr/bin/margin-offset.ps \"%s\"";

## loglevel for cups-x2go, possible values: emerg, alert, crit, err, warning, notice, info, debug
my $strloglevel = "notice";

# override hardcoded settings via config file
my $cfgfile="/etc/cups/cups-x2go.conf";

my $userName;
my @sessions;
my $this_host=hostname();

sub readconfig
{
	if( -e $cfgfile)
	{
		open(CFG,"<$cfgfile");
		while(!eof(CFG))
			{
				my $ln=<CFG>;
				my $nocomln=(split("#",$ln))[0];
				$nocomln=~s/\n//g;
				my @valarr=split("=",$nocomln);
				my $option=@valarr[0];
				shift(@valarr);
				my $value=join("=",@valarr);
				$option=~s/ //g;
				if($option eq "x2goserver")
				{
					$x2goserver=$value;
					$x2goserver=~s/ //g;
					$x2goserver=~s/\"//g;
				}
				if($option eq "printdsa")
				{
					$printdsa=$value;
				}
				if($option eq "ps2pdf")
				{
					$ps2pdf=$value;
				}
				if($option eq "loglevel")
				{
					$strloglevel=$value;
					$strloglevel=~s/ //g;
				}
			}
		close(CFG);
	}
}


sub setmylogmask {
	my $loglevel = LOG_ERR;
	if    ( $strloglevel eq "emerg" )  { $loglevel = LOG_EMERG; }
	elsif ( $strloglevel eq "alert" )  { $loglevel = LOG_ALERT; }
	elsif ( $strloglevel eq "crit" )   { $loglevel = LOG_CRIT; }
	elsif ( $strloglevel eq "err" )    { $loglevel = LOG_ERR; }
	elsif ( $strloglevel eq "warning" )   { $loglevel = LOG_WARNING; }
	elsif ( $strloglevel eq "notice" ) { $loglevel = LOG_NOTICE; }
	elsif ( $strloglevel eq "info" )   { $loglevel = LOG_INFO; }
	elsif ( $strloglevel eq "debug" )  { $loglevel = LOG_DEBUG; }
	setlogmask( LOG_UPTO( $loglevel ) );
}

sub getsessions
{
	my $sesslist;
	if ( $x2goserver eq "local" )
	{
		syslog("debug", "Querying local X2Go server for a session list...");

		# run x2golistsessions locally
		$sesslist=`su $userName -c "x2golistsessions --all-servers"`;
	}
	else
	{
		syslog("debug", "Querying remote X2Go server $x2goserver for a session list...");
		# Calling x2goprint with a single parameter <username> will result in an
		# x2golistsessions --all-servers command on the remote X2Go server.
		$sesslist=`ssh -i $printdsa x2goprint\@$x2goserver "sudo x2goprint $userName"`;
	}
	syslog("debug", $sesslist);
	@sessions=split("\n",$sesslist);
}


sub printfile
{
	my ($pfile,$tfile,$x2gosession_host, $sess)=@_;
	my $bname=basename($pfile);
	if($x2gosession_host eq $this_host)
	{
		my ($tm,$tm,$uid,$gid,$tm,$tm,$tm,$spooldir)=getpwnam("x2goprint");
		my $spfile="$spooldir/${sess}_$bname";
		copy($pfile, $spfile);
		copy($tfile, "$spfile.title");
		chown $uid,$gid,"$spfile";
		chown $uid,$gid,"$spfile.title";
		# log that we are about to process a print job
		syslog('debug', "x2goprint $userName $sess ${sess}_$bname ${sess}_$bname.title");

		# process print job
		system( "x2goprint $userName $sess ${sess}_$bname ${sess}_$bname.title" );
	}
	else
	{
		# push spool job to remote X2Go server... and launch x2goprint there (with sudo!!!)

		# PDF file
		syslog('debug', "scp -i $printdsa $pfile x2goprint\@$x2gosession_host:~x2goprint/${sess}_$bname");
		system ("scp -i $printdsa $pfile x2goprint\@$x2gosession_host:~x2goprint/${sess}_$bname");
		# title file
		syslog('debug', "scp -i $printdsa $tfile x2goprint\@$x2gosession_host:~x2goprint/${sess}_$bname.title");
		system ("scp -i $printdsa $tfile x2goprint\@$x2gosession_host:~x2goprint/${sess}_$bname.title");
		# run x2goprint on remote X2Go server
		syslog('debug', "ssh -i $printdsa  x2goprint\@$x2gosession_host \"sudo x2goprint $userName $sess ${sess}_$bname ${sess}_$bname.title\"" );
		system( "ssh -i $printdsa  x2goprint\@$x2gosession_host \"sudo x2goprint $userName $sess ${sess}_$bname ${sess}_$bname.title\"" );

	}
}


### main ###

if (!$ARGV[0])
{
	print "file cups-x2go:/ \"Virtual X2Go Printer\" \"CUPS-X2Go\" \"MFG:Generic;MDL:CUPS-X2Go Printer;DES:Generic CUPS-X2Go Printer;CLS:PRINTER;CMD:POSTSCRIPT;\"\n";
	exit 0;
}

# read config file before we go on...
readconfig();
setmylogmask();

if (scalar(@ARGV) < 5 || scalar(@ARGV) > 6)
{
	print STDERR "ERROR: Usage: cups-x2go job-id user title copies options [file]\n";
	exit 1;
}

my $jobID;
my $jobTitle;
my $copies;
my $printOptions;
my $psFile;

($jobID, $userName, $jobTitle, $copies, $printOptions, $psFile) =  @ARGV;
syslog('notice', "Print job received from cups -> $jobID $userName $jobTitle $copies $printOptions $psFile");

my $jid = $jobID;
my $uid = $userName;
$jid =~ s/\W//g; #sanity check
$uid =~ s/\W//g; #sanity check

my $template = "$jid-$uid-cupsjob$$" . "X" x 16;
my $need_ps_file_cleanup = 0;
if (!$psFile)
{
	$need_ps_file_cleanup = 1;

	my ($tfh, $tempFile) = tempfile ($template, UNLINK => 0, TMPDIR => 1);
	syslog('info', "Print job comes from STDIN, writing incoming job to temp file $tempFile\n");

	binmode ($tfh, ":raw");
	binmode (STDIN, ":raw");

	# Get all input from CUPS.
	{
		# Force readline to not read "lines", but raw data, 8192 bytes at a time.
		local $/ = \8192;

		while (!eof (STDIN)) {
			if (defined (my $stdin_data = readline (*STDIN))) {
				print $tfh $stdin_data;
			}
			else {
				if ($!{EAGAIN} || $!{EWOULDBLOCK} || $!{EINTR}) {
					next;
				}
				else {
					syslog ('err', "Error while reading data from stdin: $!\n");
					die ("Error while reading data from stdin: $!\n");
				}
			}
		}
	}

	close ($tfh);

	$psFile = $tempFile;
}

# converting PS file that we retrieved from CUPS into PDF format
my @ps2pdf_args = parse_line (" ", 0, $ps2pdf);
my $ps2pdf_cmd = shift (@ps2pdf_args);

for (my $i = 0; $i < @ps2pdf_args; ++$i) {
	$ps2pdf_args[$i] =~ s/%s/$psFile/g;
}

syslog('info', "Converting printjob with command: $ps2pdf_cmd " . join (" ", @ps2pdf_args) . "\n");

my ($pdf_fh, $pdfFile) = tempfile ($template, UNLINK => 1, TMPDIR => 1, SUFFIX => '.pdf');

# the TMPDIR env var is needed for ghostscript...
$ENV{TMPDIR}="/tmp";
my ($gs_out, $gs_in, $wait_ret, $real_ret);
my $gs_pid = open2 ($gs_out, $gs_in, $ps2pdf_cmd, @ps2pdf_args);

binmode ($gs_out, ":raw");
binmode ($pdf_fh, ":raw");

close ($gs_in);

# Get all input from GS.
{
	# Force readline to not read "lines", but raw data, 8192 bytes at a time.
	local $/ = \8192;

	while (!eof ($gs_out)) {
		if (defined (my $gs_out_data = readline ($gs_out))) {
			print $pdf_fh $gs_out_data;
		}
		else {
			if ($!{EAGAIN} || $!{EWOULDBLOCK} || $!{EINTR}) {
				next;
			}
			else {
				syslog ('err', "ERROR: reading data from ps2pdf program failed: $!\n");
				die ("$0: Error reading data from ps2pdf program failed: $!\n");
			}
		}
	}
}

close ($gs_out);

waitpid ($gs_pid, 0);

$wait_ret = $?;
$real_ret = $wait_ret >> 8;

my $conv_fail = 0;
if (-1 == $wait_ret) {
	syslog('err', "ERROR: cups-x2go failed to spawn ps2pdf conversion process: $!");
	$conv_fail = 1;
}
elsif ($wait_ret & 127) {
	my $signal = $wait_ret & 127;
	syslog('err', "ERROR: cups-x2go failed to execute ps2pdf conversion program: interrupted by signal $signal");
	$conv_fail = 1;
}
else {
	if (0 != $real_ret) {
		syslog('err', "ERROR: ps2pdf conversion program failed to convert $psFile: failed with exit code $real_ret");
		$conv_fail = 1;
	}
}

if ($conv_fail) {
	syslog('err', "ERROR: cups-x2go failed to process postscript file $psFile");
	die "$0: Failed to convert postscript file $psFile";
}

syslog('debug', "cups-x2go processed postscript file $psFile to $pdfFile");

# After we have created the PDF from CUPS's PS file, we can drop the PS file.
# If we read data from STDIN, though, let the END block handle this cleanup.
if (!$need_ps_file_cleanup) {
	unlink ($psFile);
}

my ($title_fh, $titleFile) = tempfile ($template, UNLINK => 1, TMPDIR => 1, SUFFIX => '.pdf.title');
print $title_fh $jobTitle;
close ($title_fh);

getsessions();
syslog('debug', "Retrieved session list: @sessions\n");

for(my $i=0; $i<scalar(@sessions);$i++ )
{
	my @sinfo=split("\\|",@sessions[$i]);
	if(@sinfo[4] eq "R")
	{
		syslog('debug', "Call to printfile function with: $pdfFile $titleFile @sinfo[3] @sinfo[1]\n");
		printfile( $pdfFile, $titleFile, @sinfo[3], @sinfo[1]);
	}
}

# Temporary PDF file cleanup is hopefully handled correctly by File::Temp.

END {
	if ($need_ps_file_cleanup) {
		unlink $psFile or syslog ('warning', "WARNING: cups-x2go: unable to delete temporary postscript file $psFile\n");
	}

	# closing syslog
	closelog;
}
