# XEP-0027 Plugin for Pidgin (GTK).
#
# This plugin implements encryption and decryption of
# jabber messages according to XEP-0027.
# It does not sign nor verify <presence> or <status> messages,
# as these only indicate that XEP-0027 is present at the remote party.
#
# Configuration:
#  * configure gpg-agent manually
#  * make sure gpg and gpg-agent are in %PATH%
#  * JID => GPG key mapping is done search for jid in gpg,
#    but may be overridden using config dialog.
#
# I don't take any liabilitity.
#
#    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 3 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, see <http://www.gnu.org/licenses/>.
#
# (C) 2008, Michael Braun <michael-dev@fami-braun.de>

use strict;
use File::Temp qw /tempfile tmpnam/;
use Purple;
use File::Touch;
use Config::INI::Simple;
#use Pidgin;

use constant TRUE => 1  ;
use constant FALSE => 0 ;
use constant CONFIGFILENAME => "pidgin-openpgp.ini";

# *********** PLUGIN META DATA *************
our %PLUGIN_INFO = (
    perl_api_version => 2,
    name => "OpenPGP Plugin",
    version => "0.1",
    summary => "Send and receive gpg encrypted messages.",
    description => "XEP-0027",
    author => "Michael Braun <michael-dev\@fami-braun.de>",
    url => "http://www.fami-braun.de",
    load => "plugin_load",
    unload => "plugin_unload",
    prefs_info => "prefs_info_cb",
);

my %CONNSTATE = ();
my %GPGMAP = ();
my $gpg = qx(gpg-agent --daemon)."gpg";

sub plugin_init {
    return %PLUGIN_INFO;
}

# *********** DECODE received messages **********
# TODO: fork into background, timeout, check return code of gpg, asking for key
#       send error message on decryption failure
sub decrypt {
    my $ciphertext = shift;
    my ($input, $fni) = tempfile();
    chmod 0600, $fni;

    print $input "-----BEGIN PGP MESSAGE-----\n\n$ciphertext\n-----END PGP MESSAGE-----\n";
    close $input;

    my $fno = tmpnam();

    my $ret = system("$gpg --batch --output $fno --use-agent -q -d $fni");

    if ($ret != 0) {
       return "*decryption failed*";
    }

    my $output;
    open $output, "<", $fno;
    my @plain = <$output>;
    close $output;

    unlink $fni;
    unlink $fno;

    return join("",@plain);
}

#******* verify received messages *******
# TODO: implement

# *********** INCOMING handler *************
# TODO: indicate encryption state of message, replace body content,
# OPTIONAL: detect signed presence and status tags and inform the user about
#           the remote capabilites
sub conv_receiving_jabber
{
    my ($conn, $node, $data) = @_;
    my $encrypted_node = $node->get_child_with_namespace("x","jabber:x:encrypted");
    my $body = $node->get_child("body");
    if (not defined($encrypted_node)) {
       Purple::Debug::misc("         *    ", "no opengpg message");
       if (defined($body)) {
          my $newmsg ="?NOGPG?";
          $node->get_child("body")->insert_data($newmsg,length($newmsg));
       }
    } else {
       Purple::Debug::misc("opengpg received", $conn->get_display_name().", ".$node->get_attrib("id").", $data\n");
       my $crypted = $encrypted_node->get_data();
       my $plaintext = decrypt($crypted);
       #does not work: results in no message to be shown
       #$node->get_child("body")->free();
       #$node->new_child("body");
       my $newmsg = "?PGP?$plaintext";
       $node->get_child("body")->insert_data($newmsg,length($newmsg));
    }
    @_[2] = $node;
    Purple::Debug::misc("opengpg received", $node->to_str(0)."\n");
    #return $node;
}

sub conv_receiving_msg
{
       my ($account, $from, $message, $conv, $flags, $data) = @_;
       my $xm = @_[2];

       Purple::Debug::misc("received", "$message\n");
       $message =~s/<body>(.*)<\/body>/$1/;

       if ($message =~/\?NOGPG\?$/) {
          $message =~s/(.*)\?NOGPG\?$/<i><span title="unencrypted">[U]<\/span><\/i> $1/;
       } else {
          $message =~s/.*\?PGP\?/<i><span title="encrypted">[E]<\/span><\/i> /;
       }

       $message = "<body>".$message."</body>";
       @_[2] = $message;
       Purple::Debug::misc("openpgpplugin", "replaced: $message\n");
}

# *********** ENCRYPT outgoing messages ************
# TODO: fork into background, display error message
sub encrypt {
    my $plaintext = shift;
    my $target = shift;

    if (exists($GPGMAP{$target})) {
      $target = $GPGMAP{$target};
    }

    my ($input, $fni) = tempfile();
    chmod 0600, $fni;

    print $input $plaintext;
    close $input;

    my $fno = tmpnam();

    my $ret = system("$gpg --batch --output $fno --use-agent -q --armor -r \"$target\" -e $fni");

    my $output;
    open $output, "<", $fno;
    my @plain = <$output>;
    chomp(@plain);
    close $output;

    unlink $fni;
    unlink $fno;

    if ($ret > 0) {
       Purple::Debug::misc("openpgp","encryption failed\n");
       return "";
    }

    # find first empty line
    for (; not ($plain[0] eq "");) {shift(@plain);};
    shift(@plain);
    pop(@plain);
    Purple::Debug::misc("openpgp","encryption successfull\n");

    return join("\n",@plain);
}

# *********** SIGN outgoing messages *********
# TODO: implement
#
# ********** OUTGOING handler ************
# encrypt outgoing message nodes and sign outgoing status and presence nodes
# TODO: implement, configure key to use

sub info_enable_gpg {
  my $target = shift;
  require Gtk2;
  my $frame = Gtk2::Window->new();
  my $dialog = Gtk2::MessageDialog->new ($frame,
                                      'destroy-with-parent',
                                      'info', # message type
                                      'ok', # which set of buttons?
                                      "Encryption for $target enabled.");
  $dialog->run;
  $dialog->destroy;
  $frame->destroy;
}

sub info_disable_gpg {
  my $target = shift;
  require Gtk2;
  my $frame = Gtk2::Window->new();
  my $dialog = Gtk2::MessageDialog->new ($frame,
                                      'destroy-with-parent',
                                      'info', # message type
                                      'ok', # which set of buttons?
                                      "Encryption for $target disabled.");
  $dialog->run;
  $dialog->destroy;
  $frame->destroy;
}

sub info_err_encrypt {
  my $target = shift;

  require Gtk2;
  my $frame = Gtk2::Window->new();
  my $dialog = Gtk2::MessageDialog->new ($frame,
                                      'destroy-with-parent',
                                      'error', # message type
                                      'cancel', # which set of buttons?
                                      "Could not encrypt message for $target.\nPlease check gpg settings and verify gpg-agent is running.");
  $dialog->run;
  $dialog->destroy;
  $frame->destroy;
}

sub conv_sending_msg
{
    my ($conn, $node, $data) = @_;

    # get text node
    my $bnode = $node->get_child("body");
    if (not defined($bnode)) { return; }

    # fetch target / connid
    my $target = $node->get_attrib("to");
    $target =~s/\/.*//; # name@host/path => remove path
    my $connid = $target;
    if (not exists($CONNSTATE{$connid})) {$CONNSTATE{$connid} = 1; } # default off
    Purple::Debug::misc("openpgp","sending to $target\n");

    # fetch message
    my $msg = $bnode->get_data();

    # parse commands, decide on encryption
    my $do_encrypt = $CONNSTATE{$connid};
    Purple::Debug::misc("openpgp","sending message = $msg\n");
    if ($msg =~/^ENABLEPGP/) {
      Purple::Debug::misc("openpgp","enable pgp\n");
      $CONNSTATE{$connid} = 0;
      $msg = "The remote party <b>enabled</b> XEP-0027 (OpenPGP) encryption.";
      $do_encrypt = 0;
      info_enable_gpg($target);
    } elsif ($msg =~/^DISABLEPGP/) {
      Purple::Debug::misc("openpgp","disable pgp\n");
      $CONNSTATE{$connid} = 1;
      $msg = "The remote party <b>disabled</b> XEP-0027 (OpenPGP) encryption.";
      info_disable_gpg($target);
    }

    if ($do_encrypt == 1) { return; }

    # drop html node
    my $htmlbnode = $node->get_child("html");
    if (defined($htmlbnode)) { $htmlbnode->free(); }

    # encrypt data
    my $crypted = encrypt($msg, $target);

    if ($crypted eq "") {
      Purple::Debug::misc("openpgp","sending error message\n");
      info_err_encrypt($target);
      #$conn->get_im_data()->write("OpenPGP", "<b>Cannot encrypt last message.</b>", 0, 0);
      #$node->free(); -> crashes.
      # remove plain data
      $msg = "Failed to encrypt message.";
      $bnode->free();
      $node->new_child("body")->insert_data($msg, length($msg));
      @_[1] = $node;
      return;
    }

    # insert encrypted data
    my $x = $node->new_child("x");
    $x->set_attrib("xmlns","jabber:x:encrypted");
    $x->insert_data($crypted, length($crypted));

    # remove plain data
    $msg = "This is a protected copy.";
    $bnode->free();
    $node->new_child("body")->insert_data($msg, length($msg));

    # ensure new node is used!
    @_[1] = $node;
    Purple::Debug::misc("openpgp sending new", $node->to_str(0)."\n");
}

#****** modified conversation ******
# here to come: integrate into conversation window
#sub conv_switched {
#   Purple::Debug::misc("openpgpplugin", "conv switched\n");
#
#}
#
#sub conv_deleted {
#   Purple::Debug::misc("openpgpplugin", "conv deleted:".join(",",@_)."\n");
#
#}
#
#sub conv_created {
#   Purple::Debug::misc("openpgpplugin", "conv created:".join(",",@_)."\n");
#  my $conv = shift; # PurpleConversation
#  init_dialog($conv);
#}

sub init_dialog {
#  require Gtk2;
#  require Pidgin::IMHtmlToolbar;
#  Purple::Debug::misc("openpgpplugin", "conv init\n");
#
#  my $conv = shift;
#  my $button = Gtk2::Button->new();
#  $button->set_relief("GTK_RELIEF_NONE");
#  bbox = gtkconv->toolbar;
#
#  gtk_box_pack_start(GTK_BOX(bbox), button, FALSE, FALSE, 0);
#
#    bwbox = gtk_hbox_new(FALSE, 0);
#    gtk_container_add(GTK_CONTAINER(button), bwbox);
#    icon = otr_icon(NULL, TRUST_NOT_PRIVATE, 1);
#    gtk_box_pack_start(GTK_BOX(bwbox), icon, TRUE, FALSE, 0);
#    label = gtk_label_new(NULL);
#    gtk_box_pack_start(GTK_BOX(bwbox), label, FALSE, FALSE, 0);
#
#    if (prefs.show_otr_button) {
#        gtk_widget_show_all(button);
#    }
}

# ************ CONFIG handler **********
# configure keys to use per contact and per account / global
# TODO: implement, where to store this information
my $LOCKED = 0;
my %JIDINLINE = ();
my %ITEMS=();

sub info_err_savecfg {
  my $target = shift;

  require Gtk2;
  my $frame = Gtk2::Window->new();
  my $dialog = Gtk2::MessageDialog->new ($frame,
                                      'destroy-with-parent',
                                      'error', # message type
                                      'cancel', # which set of buttons?
                                      "Could not save config in $target.");
  $dialog->run;
  $dialog->destroy;
  $frame->destroy;
}

sub SaveCfg {
  my $cfgfile = Purple::Prefs::get_string("/plugins/core/openpgp/configfile");
  Purple::Debug::misc("openpgpplugin", "save:" .join(",",keys(%GPGMAP))." => ".join(",",values(%GPGMAP))." into $cfgfile\n");

  if (not -e $cfgfile) {
    touch($cfgfile);
  }

  if (not -w $cfgfile) {
    info_err_savecfg($cfgfile);
    return;
  }
  my $conf = new Config::INI::Simple;
  foreach my $key (keys(%GPGMAP)) {
    $conf->{default}->{$key} = $GPGMAP{$key};
  }
  $conf->write($cfgfile);
}

# file content:
# JID=key
sub LoadCfg {
  my $conf = new Config::INI::Simple;
  my $cfgfile = Purple::Prefs::get_string("/plugins/core/openpgp/configfile");
  if (not -r $cfgfile) {
   %GPGMAP = ();
   return;
  }

  $conf->read($cfgfile);
  use Data::Dumper;
  Purple::Debug::misc("openpgpplugin", Dumper($conf->{default})."\n");
  %GPGMAP = ();
  foreach my $key (keys(%{$conf->{default}})) {
    $GPGMAP{$key} = $conf->{default}->{$key};
  }
  Purple::Debug::misc("openpgpplugin", "load:" .join(",",keys(%GPGMAP))." => ".join(",",values(%GPGMAP))."\n");
}

sub delete_event {
  Purple::Debug::misc("openpgpplugin", "closing config window\n");
    # closing config window
  $LOCKED = 0;
}

sub on_ok {
  Purple::Debug::misc("openpgpplugin", "ok pressed\n");
  my $self = shift;
  my $frame = shift;
  $frame->destroy;
}

sub on_add {
  Purple::Debug::misc("openpgpplugin", "add pressed\n");
  my $self = shift;
  my $data = shift;
  my $ppref1 = $data->[0];
  my $ppref2 = $data->[1];
  my $xtable = $data->[2];
  my $jid = $ppref1->get_text();
  my $key = $ppref2->get_text();
  if (exists($GPGMAP{$jid})) {
    $GPGMAP{$jid} = $key;
    Purple::Debug::misc("openpgpplugin", "replacing $jid => $key\n");
    my $i = $JIDINLINE{$jid};
    $ITEMS{$i}->[1]->set_text($key);
    &SaveCfg;
  } else {
    my $i = keys(%GPGMAP) +2;
    Purple::Debug::misc("openpgpplugin", "adding $jid => $key with i=$i\n");
    $GPGMAP{$jid} = $key;
    $xtable->resize($i+1, 3);
    add_to_table($xtable, $jid, $i);
    &SaveCfg;
  }
  $ppref1->set_text("");
  $ppref2->set_text("");
}

sub on_del {
  Purple::Debug::misc("openpgpplugin", "del pressed\n");
  my $self = shift;
  my $data = shift;

  my $xtable = $data->[0];
  my $i = $data->[1];
  my $jid = $ITEMS{$i}->[3];

  # remove from GPGMAP
  Purple::Debug::misc("openpgpplugin", "deleting entry $i ($jid => $GPGMAP{$jid})\n");
  delete($GPGMAP{$jid});

  # move all consecutive items up
  for (my $j = $i; $j < keys(%ITEMS)+1; $j++) {
    my $jid = $ITEMS{$j+1}->[3];
    $ITEMS{$j}->[3] = $jid; # move jid down
    $ITEMS{$j}->[0]->set_text($ITEMS{$j+1}->[0]->get_text()); # move jid label down
    $ITEMS{$j}->[1]->set_text($ITEMS{$j+1}->[1]->get_text()); # move key label down
    $JIDINLINE{$jid} = $j;
  }

  # remove last line
  my $j = keys(%ITEMS)+1;
  $ITEMS{$j}->[0]->destroy;
  $ITEMS{$j}->[1]->destroy;
  $ITEMS{$j}->[2]->destroy;
  delete($ITEMS{$j});
  $xtable->resize(keys(%ITEMS)+2,3);

  &SaveCfg;
}

sub add_to_table {
  my ($xtable, $jid, $i) = @_;

  Purple::Debug::misc("openpgpplugin", "show $jid\n");

  my $ppref1 = Gtk2::Label->new("$jid");
  $xtable->attach_defaults($ppref1, 0, 1, $i, $i+1);
  $ppref1->set_selectable(TRUE);
  $ppref1->show;

  my $value = $GPGMAP{$jid};
  my $ppref2 = Gtk2::Label->new("$value");
  $xtable->attach_defaults($ppref2, 1, 2, $i, $i+1);
  $ppref2->set_selectable(TRUE);
  $ppref2->show;

  my $button = Gtk2::Button->new("Del");
  $button->signal_connect(clicked => \&on_del, [$xtable, $i]);
  $xtable->attach_defaults($button, 2, 3, $i, $i+1);
  $button->show;

  $JIDINLINE{$jid} = $i;
  $ITEMS{$i} = [$ppref1, $ppref2, $button, $jid];
}

sub prefs_info_cb {
    Purple::Debug::misc("openpgpplugin", "cb\n");
    if ($LOCKED > 0) { return; }
    $LOCKED = 1;
  # *** JID => GPG-KeyID ***
    require Gtk2;
    my $frame = Gtk2::Window->new("toplevel");
    $frame->set_title("OpenPGP Plugin Konfiguration");
    $frame->signal_connect(delete_event => \&delete_event);

    my $box1 = Gtk2::VBox->new(FALSE, 0);
    $frame->add($box1);
    $box1->show;

    my $ppref = Gtk2::Label->new("Start gpg-agent first.");
    $box1->pack_start($ppref, TRUE, TRUE, 0);
    $ppref->show;

    my $ppref = Gtk2::Label->new("Use ENABLEPGP in conversation to enable encryption.");
    $box1->pack_start($ppref, TRUE, TRUE, 0);
    $ppref->show;

    my $ppref = Gtk2::Label->new("Use DISABLEPGP in conversation to disable encryption.");
    $box1->pack_start($ppref, TRUE, TRUE, 0);
    $ppref->show;

    my $ppref = Gtk2::Label->new("This plugin will DEFAULT to the jabber-id to lookup the remote gpg key.\nThe gpg binary is searched in the common (OS-dependent) path.");
    $box1->pack_start($ppref, TRUE, TRUE, 0);
    $ppref->show;

    my $separator = Gtk2::HSeparator->new;
    $box1->pack_start($separator, TRUE, TRUE, 0);
    $separator->show;
    # *** maps ***
    my $xtable = Gtk2::Table->new(keys(%GPGMAP)+1,3,TRUE);
    $box1->pack_start($xtable, TRUE, TRUE, 0);
    $xtable->show;
    my $i = 0;

    my $ppref1 = Gtk2::Label->new("JID");
    $xtable->attach_defaults($ppref1, 0, 1, $i, $i+1);
    $ppref1->set_selectable(FALSE);
    $ppref1->show;

    my $ppref2 = Gtk2::Label->new("Key-ID");
    $xtable->attach_defaults($ppref2, 1, 2, $i, $i+1);
    $ppref2->set_selectable(FALSE);
    $ppref2->show;

    $i = $i + 1;

    my $ppref1 = Gtk2::Entry->new;
    $xtable->attach_defaults($ppref1, 0, 1, $i, $i+1);
    $ppref1->show;

    my $ppref2 = Gtk2::Entry->new;
    $xtable->attach_defaults($ppref2, 1, 2, $i, $i+1);
    $ppref2->show;

    my $button = Gtk2::Button->new("Add");
    $button->signal_connect(clicked => \&on_add, [$ppref1, $ppref2, $xtable]);
    $xtable->attach_defaults($button, 2, 3, $i, $i+1);
    $button->show;

    $i = $i + 1;

    foreach my $jid (keys(%GPGMAP)) {
       add_to_table($xtable, $jid, $i);

       $i=$i+1;
    }

    my $separator = Gtk2::HSeparator->new;
    $box1->pack_start($separator, TRUE, TRUE, 0);
    $separator->show;

    # *** buttons ****
    my $button = Gtk2::Button->new("Ok");
    $button->signal_connect(clicked => \&on_ok, $frame);
    $box1->pack_start($button, TRUE, TRUE, 0);
    $button->show;

    $frame->show;

    return undef;
}

# ****** ONLOAD *******
sub plugin_load {
    my $plugin = shift;
    Purple::Debug::misc("openpgpplugin", "plugin_load() - OpenPGP Plugin Loaded.\n");
    Purple::Prefs::add_none("/plugins/core/openpgp");
    Purple::Prefs::add_string("/plugins/core/openpgp/configfile", Purple::Util::user_dir()."/".CONFIGFILENAME);

    # A pointer to the handle to which the signal belongs needed by the callback function
    my $accounts_handle = Purple::Accounts::get_handle();
    my $jabber = Purple::Find::prpl("prpl-jabber");
    Purple::Signal::connect($jabber, "jabber-receiving-xmlnode", $plugin, \&conv_receiving_jabber, "receiving jabber node");
    Purple::Signal::connect($jabber, "jabber-sending-xmlnode", $plugin, \&conv_sending_msg, "sending jabber node");

    my $conv = Purple::Conversations::get_handle();
    Purple::Signal::connect($conv, "receiving-im-msg", $plugin, \&conv_receiving_msg, "receiving im message");
#    Purple::Signal::connect($conv, "conversation-switched", $plugin, \&conv_switched, "conversation switched");
#    Purple::Signal::connect($conv, "deleting-conversation", $plugin, \&conv_deleted, "conversation deleted");
#    Purple::Signal::connect($conv, "conversation-created", $plugin, \&conv_created, "conversation created");

   &LoadCfg();
}

# ****** ON UNLOAD *******
sub plugin_unload {
    my $plugin = shift;
    Purple::Debug::misc("openpgpplugin", "plugin_unload() - OpenPGP Plugin Unloaded.\n");
}
