#!/usr/bin/perl -w
#######################################################################
# $Id: snowsync 491 2004-12-08 12:07:50Z kiza $
#
# Snownews - A lightweight console RSS newsreader
#
# Copyright 2003-2004 Oliver Feiler <kiza@kcore.de>
# http://snownews.kcore.de/
#
# snowsync
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#######################################################################

use strict;
use English;
use Fcntl':flock';
use File::Path;
use LWP::UserAgent;					# Uploading
use LWP::Simple;					# Downloading
use HTTP::Request;					# Upload server response checking
use HTTP::Response;					# Upload server response checking
use File::Copy;

#######################################################################

my $fileversion = "version1";
my $debug = 0;

my $storage_server;
my $storage_username;
my $storage_password;
my $storage_PGP_pass;

my $sync_type;
my $sync_error = undef;

umask 077;
$| = 1;

my $snowsync_version = 1;

my $snownews_version = `snownews -V`;
   $snownews_version = (split(/ /,$snownews_version))[2];
chomp($snownews_version);

my $sysuser = $ENV{'USER'};
my $homedir = $ENV{'HOME'};
my $tmpdir = "/tmp/snowsync-".$ENV{'USER'}.".$PID.".time();

# Default to Snownews
my $dot_dir = "$homedir/.snownews";
my $config_loc = "$dot_dir/snowsync.conf";
my $log_loc = "$dot_dir/snowsync.log";
my $main_prog_name = "snownews";

if ($PROGRAM_NAME =~ /lifereasync/) {
	# Liferea
	$dot_dir = "$homedir/.liferea";
	$config_loc = "$dot_dir/liferea_sync.conf";
	$log_loc = "$dot_dir/liferea_sync.log";
	$main_prog_name = "liferea";
}

if ($ARGV[0]) {
	if ($ARGV[0] =~ /-h/)         { printhelp(); }
	elsif ($ARGV[0] eq "remote")  { $sync_type = "remote"; }
	elsif ($ARGV[0] eq "local")   { $sync_type = "local"; }
	elsif ($ARGV[0] eq "delete")  { $sync_type = "delete"; }
	else                          { printhelp(); }
} else {
	printhelp();
}

if ($PROGRAM_NAME =~ /snowsync/) {
	# We don't need this if running as lifereasync
	checkRunning();
}
loadConfig();

if ($sync_type eq "remote")    { syncRemote(); }
elsif ($sync_type eq "local")  { syncLocal(); }
elsif ($sync_type eq "delete") { syncDelete(); }

logRun();
cleanUp();
exit(0);

#######################################################################
# See if snownews is running. Works with snownews >= 1.5.6
sub checkRunning {
	my($pid_file) = $ENV{'HOME'}."/.snownews/pid";
	
	open (PID, "$pid_file") or return;
	my($my_pid) = <PID>;
	close (PID);
	
	print "Snownews seems to be running at PID $my_pid.\n".
	      "You should quit snownews before running snowsync. Synced data may be lost\n".
	      "otherwise.\n\nSync anyway (yes/no)? ";
	if (<STDIN> ne "yes\n") {
		print "Aborted.\n";
		exit(0);
	}
}

#######################################################################
# Configuration. If no config exists create a new one.
sub loadConfig {
	if (!open (CONFIG, "$config_loc")) {
		createConfig();
	} else {
		while (<CONFIG>) {
			if (/^[\[\n#]/) {
				next;
			}
			chomp;
			if (/^version/) {
				$fileversion = $_;
				next;
			}
			
			/(.*?)\s+(.*)$/;
			
			if ($1 eq "storage_server")	   { $storage_server = $2; }
			elsif ($1 eq "http_username") { $storage_username = $2; }
			elsif ($1 eq "http_password") { $storage_password = $2; }
			elsif ($1 eq "PGP_password")  { $storage_PGP_pass = $2; }
		}
		close (CONFIG);
	}
}

sub createConfig {
	my $input;

	if ($PROGRAM_NAME =~ /lifereasync/) {
		# Interactive mode does not work with Liferea
		print "Config creation error.\n";
		exit(1);
	}
	
	print "Snowsync initial configuration.\n".
	      "===============================\n\n";
	      
	print "Storage server path (http://kiza.kcore.de/software/snownews/snowsync)? ";
	$storage_server = <STDIN>;
	chomp($storage_server);
	if ($storage_server eq "") {
		$storage_server = "http://kiza.kcore.de/software/snownews/snowsync";
	}
	print "-> $storage_server\n\n";
	
	print "Storage server username? ";
	$storage_username = <STDIN>;
	chomp($storage_username);
	print "-> $storage_username\n\n";
	
	print "Storage server password? ";
	$storage_password = <STDIN>;
	chomp($storage_password);
	print "-> $storage_password\n\n";
	
	print "PGP encryption password? ";
	$storage_PGP_pass = <STDIN>;
	chomp($storage_PGP_pass);
	print "-> $storage_PGP_pass\n\n";
	
	print "Storage server: $storage_server\n".
	      "Username:       $storage_username\n".
	      "Password:       $storage_password\n".
	      "PGP password:   $storage_PGP_pass\n".
	      "OK (yes/no)? ";
	
	if (<STDIN> ne "yes\n") {
		print "Aborted.\n";
		exit(0);
	}
	print "\n\nWriting file $config_loc...\n";
	
	open (CONFIG, ">$config_loc");
	print CONFIG "version1\n\n";
	print CONFIG "[Sync server access]\n".
	             "storage_server\t$storage_server\n".
	             "http_username\t$storage_username\n".
	             "http_password\t$storage_password\n".
	             "\n";
	print CONFIG "[Encryption]\n".
	             "PGP_password\t$storage_PGP_pass\n".
	             "\n";
	close (CONFIG);
	
	print "Config created in $config_loc.\n\n";
}

# Unused
sub getDefaultPGPKey {
	my $gpg_path = `which gpg`;
	chomp($gpg_path);
	open (GPG, "$gpg_path --list-secret-keys|");
	while (<GPG>) {
		if (/^sec/) {
			/sec\s+\d{4}D\/(.*?) /;
			return "0x$1";
		}
	}
	close (GPG);
}

# Create the tar to upload to the server
sub makePkg {
	mkdir $tmpdir or die "Could not create working directory $tmpdir: $!";
	my $tmpfile = "$tmpdir/snowsync.tar";
	my $target = "$tmpfile.gpg";
	
	print "Creating $tmpfile...\n";
	if ($PROGRAM_NAME =~ /snowsync/) {
		mkpath ("$tmpdir/.snownews/cache", $debug) or die "Could not create working directories under $tmpdir: $1\n";
		copy ("$dot_dir/urls", "$tmpdir/.snownews/") or die "$1";

		opendir (CACHE, "$dot_dir/cache");
		my @files = readdir CACHE;
		closedir (CACHE);
		makeSimplifiedCache(\@files, $tmpdir);

#		my @files = glob "$dot_dir/cache/*";
#		foreach (@files) {
#			copy ($_, "$tmpdir/.snownews/cache/") or die "$!";
#		}
		
		# Ugh, wtf can't GnuPG read password from cmd line?
		# This whole read-from-fd thing is annoying, plus I have no idea
		# how to do it. Need to split tar and gpg runs.
		system ("cd /$tmpdir/; tar cf $tmpfile .snownews/ 2>/dev/null");
		system ("echo '$storage_PGP_pass' | gpg --passphrase-fd 0 -q -c -o $target $tmpfile 2>/dev/null");
		print "OK.\n";
	} elsif ($PROGRAM_NAME =~ /lifereasync/) {
		mkpath ("$tmpdir/.liferea", $debug) or die "Could not create working directories under $tmpdir: $1\n";
		copy ("$dot_dir/feedlist.opml", "$tmpdir/.liferea/") or die "$1";
		# Add more files here if needed.
		
		system ("cd /$tmpdir/; tar cf $tmpfile .liferea/ 2>/dev/null");
		system ("echo '$storage_PGP_pass' | gpg --passphrase-fd 0 -q -c -o $target $tmpfile 2>/dev/null");
		print "OK.\n";
	} else {
		unknownProgName();
	}
	
	return $target;
}

# Create cache without description
sub makeSimplifiedCache {
	my $files = shift;
	my $tmpdir = shift;

	use XML::LibXML;
	use XML::LibXSLT;
	
my $xslt_stylesheet = q{<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
                xmlns:rss="http://purl.org/rss/1.0/"
                xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
                xmlns:snow="http://snownews.kcore.de/ns/1.0/"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="rss:description" />

<xsl:template match="@*|node()">
        <xsl:copy>
                <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
</xsl:template>

</xsl:stylesheet>
};

	my $parser = XML::LibXML->new();
	my $xslt = XML::LibXSLT->new();
	
	$parser->validation(0);			# Turn off validation from libxml
	$parser->recover(1);			# Use recovery mode
	
	my $style_doc = $parser->parse_string($xslt_stylesheet);
	my $stylesheet = $xslt->parse_stylesheet($style_doc);
 	
	foreach (@$files) {
		if (/^\.+/) {
			# Filter directory index files "." ".."
			next;
		}
		my $result = $stylesheet->transform_file("$dot_dir/cache/$_");
		$stylesheet->output_file($result, "$tmpdir/.snownews/cache/$_");
	}
}

sub syncRemote {
	my $target = makePkg();
	
	# Workaround. Something like mmap would be cool.
	open (FILE, "$target");
	my $target_contents = join("", <FILE>);
	close (FILE);
	
	print "Sending file to storage server... ";
	my $ua = LWP::UserAgent->new();
	my $req = HTTP::Request->new('POST', "$storage_server/nwstorage.cgi?mode=post&resource=$storage_server/data/$storage_username/$main_prog_name.pgp", undef, $target_contents);
	my $uri = URI->new($storage_server);
	my $hostname = $uri->host_port;
	$ua->credentials($hostname,
	                 'Snowsync',
	                 $storage_username,
	                 $storage_password);
	my $resp = $ua->request($req);
	print "OK.\n";
	
	if ($resp->code() == 201) {
		print "Server reply: 201 Created... OK.\n\n";
#		print $resp->content()."\n";
	} else {
		print "Sending failed. Server reply below.\n\n";
		print $resp->as_string()."\n\n";
		print "Please report this problem to the storage server admin.\n\n";
		$sync_error = "Sync failed. Storage server error (HTTP ".$resp->code().")";
	}
	print "Sync done.\n";
}
sub syncLocal {
	mkdir $tmpdir or die "Could not create working directory $tmpdir: $!";
	
	print "Fetching file from storage server... ";
	my $target = "$tmpdir/snowsync.tar.gpg";
	my $status = getstore("$storage_server/data/$storage_username/$main_prog_name.pgp", $target);
	
	if (is_success($status)) {
		print "OK\n";
		print "Extracting archive...\n";
		system ("cd $homedir; echo '$storage_PGP_pass' | gpg --passphrase-fd 0 -q -d $target 2>/dev/null | tar xf - 2>/dev/null");
		print "OK.\n";
	} else {
		print "FAILED!\n";
		print "Server status code: $status\n";
		print "Please report this problem to the storage server admin.\n\n";
		$sync_error = "Sync failed. Storage server error (HTTP $status)";
	}
}

sub cleanUp {
	print "Cleaning up...\n";
	rmtree ($tmpdir, $debug);
}

sub syncDelete {
	print "Deleting remote file... ";
	my $ua = LWP::UserAgent->new();
	my $req = HTTP::Request->new('POST', "$storage_server/nwstorage.cgi?mode=delete&resource=$storage_server/data/$storage_username/$main_prog_name.pgp", undef, undef);
	my $uri = URI->new($storage_server);
	my $hostname = $uri->host_port;
	$ua->credentials($hostname,
	                 'Snowsync',
	                 $storage_username,
	                 $storage_password);
	my $resp = $ua->request($req);
	
	if ($resp->code() == 200) {
		print "OK.\n";
	} else {
		print "\n\nDeleting failed.\n\n";
		print $resp->as_string()."\n\n";
		$sync_error = "Deleting of sync data failed (HTTP ".$resp->code().")";
	}
}

#######################################################################
# Logging
sub print_timestring {
        my($sec,$min,$std,$mtag,$mon,$jahr,$wtag,$jtag,$somzeit) = localtime($_[0]);

        $jahr += 1900;
        $mon++;
        $mon = sprintf("%02d", $mon % 100);
        $mtag = sprintf("%02d", $mtag % 100);
        $std = sprintf("%02d", $std % 100);
        $min = sprintf("%02d", $min % 100);
        $sec = sprintf("%02d", $sec % 100);
        
        return "$jahr-$mon-$mtag, $std:$min:$sec";
}
sub logRun {
	open (LOG, ">>$log_loc");
	flock (LOG, LOCK_EX);
	
	print LOG print_timestring(time);
	if (defined($sync_error)) {
		print LOG ": $sync_type sync with $storage_server failed: $sync_error.\n";
	} else {
		print LOG ": $sync_type sync with $storage_server successful.\n";
	}	
	
	flock (LOG, LOCK_UN);
	close (LOG);
}

sub printhelp {
	if ($PROGRAM_NAME =~ /snowsync/) {
		print "Snowsync options\n\n";
	} elsif ($PROGRAM_NAME =~ /lifereasync/) {
		print "Lifereasync options\n\n";
	} else {
		unknownProgName();
	}
	print "\"snowsync local\"      Sync local configuration FROM server.\n".
	      "\"snowsync remote\"     Send local configuration TO server.\n".
	      "\"snowsync delete\"     Delete remote data.\n\n";
	      
	exit(0);
}

sub unknownProgName {
	print "The script must be named either snowsync or lifereasync!\n";
	exit(1);
}
