#!/usr/bin/perl

=head1 NAME

OpenPGP_Applet - GNOME applet for OpenPGP text encryption

=head1 VERSION

Version 1.0

=head1 DESCRIPTION

OpenPGP Applet allows encryption and decryption of the clipboard's content with
a symmetric cipher using a passphrase. This is a graphical frontend on top of
GnuPG.

Asymmetric decryption and clipboard verification are also supported.

=head1 PREREQUISITES

OpenPGP Applet does not handle passphrase input. Since it also does not
offer terminal interaction unless explicitly run from there, it relies
in practice on some kind of GnuPG agent such as pinentry, Seahorse 2.x
or GNOME keyring 3.x to manage passphrase input.

=head1 SEE ALSO

User documentation, with screenshots, can be found on https://tails.boum.org/doc/encryption_and_privacy/gpgapplet/

=head1 AUTHOR

Tails developers <tails@boum.org>

=head1 LICENCE

This program is free software; you can redistribute it and/or modify it under the terms of either:

a) the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version, or

b) the "Artistic License" which comes with Perl.

Pixmaps and icons are licensed under the terms of Creative Commons ShareAlike 2.0 (CC-BY-SA-2.0)

See README and LICENSE for details.

=cut

use strict;
use warnings FATAL => 'all';
use 5.10.0;

our $VERSION = 1.1;

my $DEBUG = $ENV{'DEBUG'};

use Glib qw{TRUE FALSE};
use Gtk3 qw{-init};
use Gtk3::SimpleList;


use Crypt::OpenPGP_Applet::GnuPG::Interface;
use DateTime;
use Encode qw{decode encode find_encoding};
use English;
use Errno qw(EPIPE);
use File::ShareDir;
use I18N::Langinfo qw{langinfo CODESET};
use IO::Handle;
use IO::Select;
use List::MoreUtils qw{none};
use POSIX;

use Locale::TextDomain ("OpenPGP_Applet");
setlocale(LC_MESSAGES, "");

=head1 GLOBALS

=cut

use constant C_SELECT      => 0;
use constant C_NAME        => 1;
use constant C_KEYID       => 2;
use constant C_STATUS      => 3;
use constant C_FINGERPRINT => 4;
use constant C_USERIDS     => 5;
use constant C_TRUSTED     => 6;
use constant VISIBLE_COLS  => (C_NAME, C_KEYID, C_STATUS);
use constant HIDDEN_COLS   => (C_FINGERPRINT, C_USERIDS, C_TRUSTED);

use constant COMBO_NAME        => 0;
use constant COMBO_KEYID       => 1;
use constant COMBO_FINGERPRINT => 2;
use constant COMBO_ROLE        => 3;

my $gnupg         = Crypt::OpenPGP_Applet::GnuPG::Interface->new();
$gnupg->call('gpg2');
my $codeset       = langinfo(CODESET());
my $encoding      = find_encoding($codeset);
my $main_window   = Gtk3::Window->new();
my $icon_factory  = Gtk3::IconFactory->new();
# Set always_trust since GnuPG otherwise will fail if the key's
# trust hasn't been set.
my %gnupg_options = (armor => 1, always_trust => 0, meta_interactive => 0);

my $pgp_encrypted_msg = {
    type   => 'message',
    header => '-----BEGIN PGP MESSAGE-----',
    footer => '-----END PGP MESSAGE-----'
};
my $pgp_signed_msg = {
    type   => 'signed',
    header => '-----BEGIN PGP SIGNED MESSAGE-----',
    middle => '-----BEGIN PGP SIGNATURE-----',
    footer => '-----END PGP SIGNATURE-----'
};
my @pgp_headers = ($pgp_encrypted_msg, $pgp_signed_msg);

=head1 MAIN

=cut

my $statusicon = build_statusicon();
$statusicon->set_visible(TRUE);
init_freshest_clipboard();
init_icons_stock($icon_factory);
detect_received(freshest_clipboard());
Gtk3->main;


=head1 FUNCTIONS

=cut

sub all_clipboards {
    map {
        Gtk3::Clipboard::get($_)
    } (
        Gtk3::Gdk::Atom::intern('CLIPBOARD', Glib::FALSE),
        Gtk3::Gdk::Atom::intern('PRIMARY', Glib::FALSE)
    );
}

{
    my $freshest_clipboard;

    sub init_freshest_clipboard {
        $freshest_clipboard = Gtk3::Clipboard::get(Gtk3::Gdk::Atom::intern('CLIPBOARD', Glib::FALSE));
    }

    sub freshest_clipboard {
        return $freshest_clipboard;
    }

    sub set_freshest_clipboard {
        $freshest_clipboard = shift;
    }
}

sub app_exit {
    my $parent = shift;
    my $dialog = Gtk3::MessageDialog->new($parent, [qw/modal destroy-with-parent/],
                                   'warning',
                                   'yes-no',
                                   $encoding->decode(__("You are about to exit OpenPGP Applet. Are you sure?")));

    $dialog->set_default_response('no');
    Gtk3->main_quit if ($dialog->run eq 'yes');

    $dialog->destroy;
}

sub build_statusicon {
    my $icon = Gtk3::StatusIcon->new;
    $icon->set_visible(FALSE);
    $icon->set_from_icon_name('seahorse');
    $icon->set_tooltip_text($encoding->decode(__("OpenPGP encryption applet")));

    my $menu   = Gtk3::Menu->new;
    my $mexit  = Gtk3::MenuItem->new_with_mnemonic($encoding->decode(__("Exit")));
    $mexit->signal_connect('activate' => sub { app_exit($main_window); });
    my $mabout = Gtk3::MenuItem->new_with_mnemonic($encoding->decode(__("About")));
    $mabout->signal_connect('activate' => sub { Gtk3->show_about_dialog(
        $main_window,
        'program-name' => 'OpenPGP Applet',
        'license'      => q{This program is free software; you can redistribute it and/or modify it under the terms of either:

a) the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version, or

b) the "Artistic License" which comes with Perl.

The pixmaps and icons are licensed using the Creative Common 1.0 Universal (CC0). Humans may want to refer to http://creativecommons.org/publicdomain/zero/1.0/

Please see README and LICENSE files distributed along this program for detail.
},
        'wrap-license' => 1,
        'website'      => 'https://tails.boum.org/',
    )});
    $menu->append($mabout);
    $menu->append(Gtk3::SeparatorMenuItem->new);
    $menu->append($mexit);

    $icon->signal_connect('popup-menu', sub {
        my $ticon = shift;
        my $event = shift;
        my $time = shift;
        $menu->show_all;
        $menu->popup(undef, undef, undef, undef, $event, $time);
    });

    $icon->signal_connect('button-press-event' => sub {
        my $ticon = shift;
        my $event = shift;
        return unless $event->button == 1;
        our $action_menu = build_action_menu();
        $action_menu->show_all;
        $action_menu->popup(undef, undef, undef, undef, $event->button, $event->time);
    });

    foreach (all_clipboards()) {
        $_->signal_connect("owner-change" => sub {
            my $clipboard = shift;
            my $event     = shift;
            handle_clipboard_owner_change($clipboard);
        });
    }

    return $icon;
}

sub build_action_menu {
    my $action_menu = Gtk3::Menu->new;

    my $text_type = detect_text_type(get_validated_clipboard_text());

    if ($text_type eq 'text' or $text_type eq 'none') {
        my $msymencrypt    = Gtk3::MenuItem->new_with_mnemonic($encoding->decode(__("Encrypt Clipboard with _Passphrase")));
        $msymencrypt->signal_connect('activate' => sub { operate_on_clipboard(\&symmetric_encrypt, ['text']); });
        $action_menu->append($msymencrypt);
        my $msignencrypt    = Gtk3::MenuItem->new_with_mnemonic($encoding->decode(__("Sign/Encrypt Clipboard with Public _Keys")));
        $msignencrypt->signal_connect('activate' => sub { operate_on_clipboard(\&public_crypto, ['text']); });
        $action_menu->append($msignencrypt);
    }
    if ($text_type eq 'message' or $text_type eq 'signed') {
        my $mdecryptver = Gtk3::MenuItem->new_with_mnemonic($encoding->decode(__("_Decrypt/Verify Clipboard")));
        $mdecryptver->signal_connect('activate' => sub { operate_on_clipboard(\&decrypt_verify, ['message', 'signed']); });
        $action_menu->append($mdecryptver);
    }
    my $mmanage = Gtk3::MenuItem->new_with_mnemonic($encoding->decode(__("_Manage Keys")));
    $mmanage->signal_connect('activate' => sub { manage_keys(); });
    $action_menu->append($mmanage);

    my $mtexteditor = Gtk3::MenuItem->new_with_mnemonic($encoding->decode(__("_Open Text Editor")));
    $mtexteditor->signal_connect('activate' => sub { open_text_editor(); });
    $action_menu->append($mtexteditor);

    return $action_menu;
}

sub manage_keys {
    system("seahorse &");
}

sub open_text_editor {
    system("gnome-text-editor &");
}

sub all_text_types {
    map { $_->{type} } @pgp_headers;
}

sub detect_text_type {
    my $text = shift;

    unless (defined $text && length($text)) {
        return 'none';
    }

    foreach (@pgp_headers) {
        my $header = $_->{header};
        my $footer = $_->{footer};
        return $_->{type} if $text =~ m{$header.*$footer}ms;
    }

    return 'text';
}

sub text_is_of_type {
    my $text        = shift;
    my @valid_types = @_;

    my $text_type = detect_text_type($text);
    if (none { $_ eq $text_type } @valid_types) {
        return (
            0,
            $encoding->decode(
                __("The clipboard does not contain valid input data."))
        );
    }
    return (1);
}

sub get_validated_clipboard_text {
    my $args = shift;
    my @valid_types;

    if (exists $args->{valid_types} && defined $args->{valid_types}) {
        @valid_types = @{ $args->{valid_types} };
    }
    else {
        @valid_types = all_text_types();
    }

    my $clipboard = freshest_clipboard();
    # FIXME-GTK3 - still true with gtk3 ?
    # Note: according to the GTK documentation, the wait_for_text method
    # is supposed to always returns UTF-8. But it seems like the Perl
    # bindings decode it and we get a string of chars instead of bytes.
    my $content = $clipboard->wait_for_text;
    my ($is_valid, $reason) = text_is_of_type($content, @valid_types);
    return ($content) if $is_valid;
    return (0, $reason);
}

sub set_clipboards_text {
    my $text = shift;
    my $encoded_text = $encoding->encode($text);

    foreach (all_clipboards()) {
        $_->set_text($encoded_text,-1);
    }
}

sub get_status {
    my $code = shift;
    my $status;
    my $trusted;
    # Below taken from doc/DETAILS in GnuPG's sources
    SWITCH: 
    for ($code){
        if ($_ eq "o") { $trusted = FALSE;
                   $status = $encoding->decode(__("Unknown Trust")); last SWITCH; }
        if ($_ eq "-") { $trusted = FALSE;
                    $status = $encoding->decode(__("Unknown Trust")); last SWITCH; }
        if ($_ eq "q") { $trusted = FALSE;
                    $status = $encoding->decode(__("Unknown Trust")); last SWITCH; }
        if ($_ eq "m") { $trusted = FALSE;
                    $status = $encoding->decode(__("Marginal Trust")); last SWITCH; }
        if ($_ eq "f") { $trusted = TRUE;
                    $status = $encoding->decode(__("Full Trust")); last SWITCH; }
        if ($_ eq "u") { $trusted = TRUE;
                    $status = $encoding->decode(__("Ultimate Trust")); last SWITCH; }
    	return;
    }
    return ($status, $trusted);
}

sub get_private_key_status {
    my $key = shift;

    my $fingerprint = $encoding->decode($key->fingerprint->as_hex_string());
    my $pubkey = ($gnupg->get_public_keys_light($fingerprint))[0]; # ignore collisions

    # a valid key may lack signing capabilities
    return unless $pubkey->usage_flags =~ m/S/;

    my $validity = $pubkey->user_ids->[0]->validity;
    return get_status($validity);
}

sub get_public_key_status {
    my $key = shift;

    # a valid key may lack encryption capabilities
    return unless $key->usage_flags =~ m/E/;

    my $validity = $key->user_ids->[0]->validity;
    return get_status($validity);
}

sub create_key_row {
    my $key = shift;

    my ($status, $trusted) = (ref($key) eq "GnuPG::SecretKey") ?
        get_private_key_status($key)
      : get_public_key_status($key);
    # no status implies expired, revoked, etc. keys, which we don't want to list
    return if !defined $status;

    my $name    = $encoding->decode($key->user_ids->[0]->as_string);
    my $userids = join("\n", map { my $a = $_->as_string; my $b = $encoding->decode("$a"); my $c ="x" . "$b" }
                                 $key->user_ids);
    my $keyid   = $encoding->decode($key->short_hex_id);

    my $fingerprint = $encoding->decode($key->fingerprint->as_hex_string());
    # Gtk3::SimpleList encodes these strings itself.
    return [FALSE, $name, $keyid, $status, $fingerprint, $userids, $trusted];
}

sub make_pub_key_list {
    my $pub_keys_ref = shift;

    my $list = Gtk3::SimpleList->new (
        ""                                    => 'bool', # C_SELECT
        $encoding->decode(__("Name"))    => 'text', # C_NAME
        $encoding->decode(__("Key ID"))  => 'text', # C_KEYID
        $encoding->decode(__("Status"))  => 'text', # C_STATUS
        ""                                    => 'text', # C_FINGERPRINT
        ""                                    => 'text', # C_USERIDS
        ""                                    => 'bool'  # C_TRUSTED
        );
    foreach my $i (VISIBLE_COLS) {
        my $col = $list->get_column($i);
        $col->set_max_width(400);
        $col->set_resizable(TRUE);
        $col->set_sort_column_id($i);
    }
    foreach my $i (HIDDEN_COLS) {
        $list->get_column($i)->set_visible(FALSE);
    }
    $list->set_search_column(C_NAME);
    $list->get_selection->set_mode('single');
    $list->get_selection->unselect_all;
    # Initially sort by name (couldn't find a cleaner way)
    $list->get_column(C_NAME)->signal_emit('clicked');

    # If we used Gtk3::TreeView instead of Gtk3::SimpleList we could
    # show all user ids directly in the list, but we make it simple
    # for us and instead show them in the tooltip.
    $list->set_has_tooltip(TRUE);
    $list->signal_connect('query-tooltip' => sub {
        my ($wx, $wy, $tooltip) = ($_[1], $_[2], $_[4]);
        my ($x, $y) = $list->convert_widget_to_bin_window_coords($wx, $wy);
        my $row = $list->get_path_at_pos($x, $y);
        return FALSE unless defined $row;
        my $fingerprint =
            join(" ", (${$list->{data}}[$row][C_FINGERPRINT] =~ m/..../g));
        my $fingerprint_label = $encoding->decode(__("Fingerprint:"));
        my $uids = "${$list->{data}}[$row][C_USERIDS]";
        my $uids_label = $encoding->decode(
            __n("User ID:", "User IDs:", ($uids =~ tr/\n//) + 1));
        my $text = sprintf("%s\n%s\n%s\n%s", $uids_label, $uids,
                           $fingerprint_label, $fingerprint);
        $tooltip->set_text("$text");
        return TRUE;
    });

    $list->signal_connect('row-activated' => sub {
        # Since we use 'single' selection mode, there can only be one
        my $index = ($list->get_selected_indices)[0];
        my $old_val = $list->{data}->[$index]->[C_SELECT];
        $list->{data}->[$index]->[C_SELECT] = !$old_val;
    });

    push @{$list->{data}},
        grep { $_ } map { create_key_row($_) } @{$pub_keys_ref};

    $list->select(0);

    return $list;
}

sub make_priv_key_combo {
    my $priv_keys_ref = shift;

    my $list_store = Gtk3::ListStore->new(
        qw/Glib::String Glib::String Glib::String Glib::String/);
    my $iter = $list_store->append;
    $list_store->set ($iter,
                      COMBO_NAME, $encoding->decode(__("None (Don't sign)")),
                      COMBO_KEYID, "",
                      COMBO_FINGERPRINT, "",
                      COMBO_ROLE, "none");
    $iter = $list_store->append;
    $list_store->set ($iter,
                      COMBO_NAME, "",
                      COMBO_KEYID, "",
                      COMBO_FINGERPRINT, "",
                      COMBO_ROLE, "separator");
    foreach my $key (@{$priv_keys_ref}) {
        my $row = create_key_row($key);
        next unless $row; # skip keys without signing capability
        $iter = $list_store->append;
        $list_store->set ($iter,
                          COMBO_NAME, "$row->[C_NAME]",
                          COMBO_KEYID, "($row->[C_KEYID])",
                          COMBO_FINGERPRINT, "$row->[C_FINGERPRINT]",
                          COMBO_ROLE, "");
    }

    my $sorted_list = Gtk3::TreeModelSort->new_with_model($list_store);
    $sorted_list->set_default_sort_func(sub {
        my ($model, $iter1, $iter2, $data) = @_;
        my $name1 = $model->get($iter1, COMBO_NAME);
        my $name2 = $model->get($iter2, COMBO_NAME);
        my $role1 = $model->get($iter1, COMBO_ROLE);
        my $role2 = $model->get($iter2, COMBO_ROLE);

        if ($role1 eq "none") {
            return -1;
        } elsif ($role2 eq "none") {
            return 1;
        } elsif ($role1 eq "separator") {
            return -1;
        } elsif ($role2 eq "separator") {
            return 1;
        } else {
            return (lc $name1 cmp lc $name2);
        }
                                });

    my $combo = Gtk3::ComboBox->new_with_model($sorted_list);
    my $renderer = Gtk3::CellRendererText->new();
    $combo->pack_start($renderer, FALSE);
    $combo->add_attribute($renderer, 'text', COMBO_NAME);
    $renderer = Gtk3::CellRendererText->new();
    $combo->pack_start($renderer, FALSE);
    $combo->add_attribute($renderer, 'text', COMBO_KEYID);
    $combo->set_row_separator_func( sub {
        my ($model, $iter, $data) = @_;
        return TRUE if ($model->get($iter, COMBO_ROLE) eq "separator");
                                        });
    $combo->set_active(0);

    return $combo;
}

sub choose_keys {
    my $priv_keys_ref = shift;
    my $pub_keys_ref = shift;

    my $pub_key_label = Gtk3::Label->new(
        $encoding->decode(__("Select recipients:")));

    my $pub_key_list = make_pub_key_list($pub_keys_ref);
    my $pub_key_list_scroll = Gtk3::ScrolledWindow->new;
    $pub_key_list_scroll->set_policy('automatic', 'always');
    $pub_key_list_scroll->add($pub_key_list);

    my $hide_recipients_checkbox = Gtk3::CheckButton->new(
        $encoding->decode(__("Hide recipients")));
    $hide_recipients_checkbox->set_has_tooltip(TRUE);
    $hide_recipients_checkbox->set_tooltip_text(
        $encoding->decode(__("Hide the user IDs of all recipients of " .
                                  "an encrypted message. Otherwise anyone " .
                                  "that sees the encrypted message can see " .
                                  "who the recipients are.")));

    my $priv_key_label = Gtk3::Label->new(
        $encoding->decode(__("Sign message as:")));

    my $priv_key_combo = make_priv_key_combo($priv_keys_ref);

    my $dialog = Gtk3::Dialog->new($encoding->decode(__("Choose keys")),
                                   $main_window, 'destroy-with-parent',
                                   'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok' );
    $dialog->set_default_size(650,500);
    $dialog->set_default_response('ok');
    my $vbox = $dialog->get_content_area;
    $vbox->pack_start($pub_key_label, FALSE, FALSE, 5);
    $vbox->pack_start($pub_key_list_scroll, TRUE, TRUE, 0);
    my $hbox = Gtk3::HBox->new;
    $hbox->pack_start($priv_key_label, FALSE, FALSE, 0);
    $hbox->pack_start($priv_key_combo, TRUE, TRUE, 0);
    $vbox->pack_start($hbox, FALSE, FALSE, 5);
    $vbox->pack_start($hide_recipients_checkbox, FALSE, FALSE, 0);

    $pub_key_list->grab_focus;
    $dialog->show_all;

    $dialog->signal_connect('key-press-event' => sub {
        my $event = $_[1];
        return unless $event->keyval == Gtk3::Gdk::KEY_Return;
        $dialog->response('ok');
        return 1;
    });

    while ($dialog->run eq 'ok') {
        my @recipients;
        my $signer;
        my $always_trust = 0;

        # Get signing key, if any
        my $priv_key_combo_model = $priv_key_combo->get_model;
        my $priv_key_iter = $priv_key_combo->get_active_iter;
        $signer = $priv_key_combo_model->get($priv_key_iter, COMBO_FINGERPRINT);

        # Get public keys, if any
        my @list_selection = grep { $_->[C_SELECT] } @{$pub_key_list->{data}};
        if (@list_selection) {
            my @unauth = grep { ! $_->[C_TRUSTED] } @list_selection;
            if (@unauth) {
                my $title = $encoding->decode(
                    __("Do you trust these keys?")
                                              );
                my $warning = $encoding->decode(__n(
                    "The following selected key is not fully trusted:",
                    "The following selected keys are not fully trusted:",
                    scalar @unauth
                                                ));
                my $msg = sprintf("%s\n", $warning);
                foreach my $key (@unauth) {
                    # Each key will be listed RTL *or* LTR depending on the
                    # direction of the first character of the name. This
                    # unfortunately causes mixing of LTR and RTL in the
                    # same list. A potential FIXME would be to display this
                    # with a custom windows using SimpleList for the keys.
                    # Also note that everything in $key (which originates
                    # from $pub_key_list) already has been decoded, so we
                    # don't have to do it again here.
                    my $key_name = "$key->[C_NAME] ($key->[C_KEYID])";
                    $msg = sprintf("%s%s\n", $msg, $key_name);
                }
                my $question = $encoding->decode(__n(
                    "Do you trust this key enough to use it anyway?",
                    "Do you trust these keys enough to use them anyway?",
                    scalar @unauth
                                                 ));
                $msg = sprintf("%s%s", $msg, $question);
                next unless display_question($dialog, $title, $msg);
                $always_trust = 1;
            }
            @recipients = map { $_->[C_FINGERPRINT] } @list_selection;
        }

        if (!@recipients && !$signer) {
            display_error($dialog,
                          $encoding->decode(__("No keys selected")),
                          $encoding->decode(__(
                              "You must select a private key to sign the " .
                              "message, or some public keys to encrypt the " .
                              "message, or both."
                                            )));
            next;
        }

        $dialog->destroy;
        return {
            always_trust => $always_trust,
            hide_recipients => $hide_recipients_checkbox->get_active,
            signer => $signer,
            recipients => \@recipients,
        };
    }
    $dialog->destroy;
    return ();
}

sub public_crypto {
    my $args    = shift;
    my $handles = $args->{handles};

    my @priv_keys = $gnupg->get_secret_keys_light();
    my @pub_keys = $gnupg->get_public_keys_light();

    if (@priv_keys == 0 && @pub_keys == 0) {
        display_error($main_window,
                      $encoding->decode(__("No keys available")),
                      $encoding->decode(__(
                          "You need a private key to sign messages or a " .
                          "public key to encrypt messages."
                                       )));
        return 0;
    }

    my $chosen = choose_keys(\@priv_keys, \@pub_keys);
    my $signer = $chosen->{signer};
    my $recipients_ref = $chosen->{recipients};
    my @recipients; @recipients = @{$recipients_ref} if defined $recipients_ref;
    my $always_trust = $chosen->{always_trust};
    my $hide_recipients = $chosen->{hide_recipients};

    $gnupg->options->always_trust($always_trust);
    $gnupg->options->clear_extra_args;
    $gnupg->options->clear_meta_signing_key_id;
    $gnupg->options->clear_recipients();

    if ($signer) {
        $gnupg->options->meta_signing_key_id($signer);
    }

    if (@recipients) {
        if ($hide_recipients) {
            # Since gpg's --no-throw-keyids seems to be broken (it doesn't
            # work via the CLI either) we can't just push it to extra_args :/.
            foreach my $recipient (@recipients) {
                $gnupg->options->push_extra_args('--hidden-recipient',
                                                 $recipient);
            }
        } else {
            $gnupg->options->push_recipients(@recipients);
        }
    }

    my $result = 0;

    if ($signer && !@recipients) {
        $result = $gnupg->clearsign(handles => $handles);
    } elsif (@recipients && !$signer) {
        $result = $gnupg->encrypt(handles => $handles);
    } elsif ($signer && @recipients) {
        $result = $gnupg->sign_and_encrypt(handles => $handles);
    }

    $gnupg->options->always_trust(0);
    $gnupg->options->clear_extra_args;
    $gnupg->options->clear_meta_signing_key_id;
    $gnupg->options->clear_recipients();

    return $result;
}

sub symmetric_encrypt {
    my $args    = shift;
    my $handles = $args->{handles};

    return $gnupg->encrypt_symmetrically(handles => $handles);
}

sub decrypt_verify {
    my $args    = shift;
    my $handles = $args->{handles};
    my $input   = $args->{input};

    my $text_type = detect_text_type($input);
    return
        $text_type eq 'message'
      ? $gnupg->decrypt(handles => $handles)
      : $gnupg->verify(handles => $handles);
}

sub gpg_operate_on_text {
    my $operation = shift;
    my $text      = shift;

    $gnupg->options->hash_init(%gnupg_options);
    my $in_h    = IO::Handle->new();
    my $err_h   = IO::Handle->new();
    my $out_h   = IO::Handle->new();


    my $handles = GnuPG::Handles->new(
        stdin => $in_h,
        stderr => $err_h,
        stdout => $out_h
    );


    my $args = {
        handles => $handles,
        input   => $text,
        in_h    => $in_h,
        err_h   => $err_h,
        out_h   => $out_h,
    };

    my $pid = $operation->($args) or return;

    my $read = _gpg_communicate([$out_h,$err_h],
                                [$in_h],
                                # We assume the sender/recipient uses the same charset as us :/
                                # PGP/MIME was invented for a reason.
                                { $in_h => $encoding->encode($text) });

    my @raw_stderr = split(/^/m, $read->{$err_h});
    my @raw_stdout = split(/^/m, $read->{$out_h});

    waitpid $pid, 0; # Clean up the finished GnuPG process.

    my $std_err = $encoding->decode(join('', @raw_stderr));
    my $std_out = $encoding->decode(join('', @raw_stdout));

    if ($CHILD_ERROR == 0) {
        if ($operation eq \&decrypt_verify) {
            my $msg;
            if ($text =~ m/$pgp_signed_msg->{header}/) {
                $msg = $text;
                $msg =~ s/^.*$pgp_signed_msg->{header}\nHash: [^\n]*\n\n//m;
                $msg =~ s/^$pgp_signed_msg->{middle}.*//ms;
            } else {
                $msg = $std_out;
            }
            display_output($msg, $std_err);
        } else {
            set_clipboards_text($std_out);
        }
    }
    else {
        display_error(
            $main_window,
            $encoding->decode(__("GnuPG error")),
            $std_out . "\n\n" . $std_err
        );
        return;
    }

    return;
}

sub operate_on_clipboard {
    my $operation   = shift;
    my $valid_types = shift;

    my ($text, $clip_error) = get_validated_clipboard_text(
        { valid_types => $valid_types }
    );

    if (defined $clip_error) {
        display_error(
            $main_window,
            $clip_error, # already translated and decoded
            $encoding->decode(__("Therefore the operation cannot be " .
                                      "performed."))
        );
        return;
    }

    gpg_operate_on_text($operation, $text);
}

sub display_error {
    my $parent = shift;
    my $title = shift;
    my $msg   = shift;

    my $dialog = Gtk3::MessageDialog->new(
        $parent, 'destroy-with-parent', 'error', 'ok',
        $title
    );
    $dialog->set('secondary_text' => $msg);
    $dialog->signal_connect(
        response => sub { my $self = shift; $self->destroy; }
    );
    $dialog->set_position('center');
    $dialog->run;
    $dialog->destroy;

    return 1;
}

sub display_question {
    my $parent = shift;
    my $title = shift;
    my $msg   = shift;

    my $dialog = Gtk3::MessageDialog->new(
        $parent, 'destroy-with-parent', 'question', 'yes-no', $title);
    $dialog->set('secondary_text' => $msg);
    $dialog->set_position('center');
    my $answer = $dialog->run;
    $dialog->destroy;
    return $answer eq 'yes' ? TRUE : FALSE;
}

# FIXME: let window grow depending on output text size
sub display_output {
    my $std_out = shift;
    my $std_err = shift;

    my $dialog = Gtk3::MessageDialog->new(
        $main_window, 'destroy-with-parent', 'info', 'ok',
        $encoding->decode(__("GnuPG results"))
    );
    my $my_width_request = 800;
    my $my_height_request = 600;
    # TRANSLATORS: GnuPG stdout (encrypted or decrypted message)
    $dialog->set('secondary_text' => sprintf($encoding->decode(
        __("Output of GnuPG:")
    )));

    my $msg_area = $dialog->get_content_area;

    my $outbuf = Gtk3::TextBuffer->new();
    $outbuf->set_text($std_out);
    my $text_desc = Pango::FontDescription->new;
    $text_desc->set_family('Monospace');
    my $textview_out = Gtk3::TextView->new_with_buffer($outbuf);
    $textview_out->set_editable(FALSE);
    $textview_out->set_cursor_visible(FALSE);
    $textview_out->set_left_margin(10);
    $textview_out->set_right_margin(10);
    $textview_out->set_wrap_mode('word');
    $textview_out->modify_font($text_desc);
    my $scrolled_win_out = Gtk3::ScrolledWindow->new;
    $scrolled_win_out->set_policy('automatic', 'automatic');
    $scrolled_win_out->add($textview_out);
    $msg_area->pack_start($scrolled_win_out, TRUE, TRUE, 0);

    if (defined $std_err && length($std_err)) {
        my $std_err_title = Gtk3::Label->new(
            # TRANSLATORS: GnuPG stderr (other informational messages)
            $encoding->decode(
               __("Other messages provided by GnuPG:")
            ));
        $std_err_title->set_alignment(0, 0);
        $std_err_title->set_padding(10, 0);
        $msg_area->pack_start($std_err_title, FALSE, FALSE, 0);
        my $std_err_buf = Gtk3::TextBuffer->new();
        $std_err_buf->set_text($std_err);
        my $textview_err = Gtk3::TextView->new_with_buffer($std_err_buf);
        $textview_err->set_editable(FALSE);
        $textview_err->set_cursor_visible(FALSE);
        $textview_err->set_left_margin(10);
        $textview_err->set_right_margin(10);
        $textview_err->set_wrap_mode('word');
        $textview_err->modify_font($text_desc);
        my $scrolled_win_err = Gtk3::ScrolledWindow->new;
        $scrolled_win_err->set_policy('automatic', 'automatic');
        $scrolled_win_err->add($textview_err);
        $scrolled_win_err->set_size_request(-1, $my_height_request/5);
        $msg_area->pack_start($scrolled_win_err, FALSE, FALSE, 0);
    }

    $dialog->signal_connect(
        response => sub { my $self = shift; $self->destroy; }
    );
    my $screen_width = $dialog->get_screen()->get_width();
    my $screen_height = $dialog->get_screen()->get_height();
    if ($screen_width > $my_width_request || $screen_height > $my_height_request) {
        $dialog->set_size_request($my_width_request, $my_height_request);
    } else {
        $dialog->maximize();
    }
    $dialog->set_resizable(TRUE);
    $dialog->set_position('center');
    $dialog->show_all;

    return 1;
}

# interleave reads and writes from gpg process
# stolen from Mail::GnuPG
# input parameters:
# $rhandles - array ref with a list of file handles for reading
# $whandles - array ref with a list of file handles for writing
# $wbuf_of - hash ref indexed by the stringified handles
# containing the data to write
# return value:
# $rbuf_of - hash ref indexed by the stringified handles
# containing the data that has been read
#
# read and write errors due to EPIPE (gpg exit) are skipped silently on the
# assumption that gpg will explain the problem on the error handle
#
# other errors cause a non-fatal warning, processing continues on the rest
# of the file handles
#
# NOTE: all the handles get closed inside this function

sub _gpg_communicate {
    my $blocksize = 2048;
    my ($rhandles, $whandles, $wbuf_of) = @_;
    my $rbuf_of = {};

    # the current write offsets, again indexed by the stringified handle
    my $woffset_of;

    my $reader = IO::Select->new;
    for (@$rhandles) {
        $reader->add($_);
        $rbuf_of->{$_} = '';
    }

    my $writer = IO::Select->new;
    for (@$whandles) {
        die("no data supplied for handle " . fileno($_)) if !exists $wbuf_of->{$_};
        if ($wbuf_of->{$_}) {
            $writer->add($_);
        } else { # nothing to write
            close $_;
        }
    }

    # we'll handle EPIPE explicitly below
    local $SIG{PIPE} = 'IGNORE';

    while ($reader->handles || $writer->handles) {
        my @ready = IO::Select->select($reader, $writer, undef, undef);
        if (!@ready) {
            die("error doing select: $!");
        }
        my ($rready, $wready, $eready) = @ready;
        if (@$eready) {
            die("select returned an unexpected exception handle, this shouldn't happen");
        }
        for my $rhandle (@$rready) {
            my $n = fileno($rhandle);
            my $count = sysread($rhandle, $rbuf_of->{$rhandle},
            $blocksize, length($rbuf_of->{$rhandle}));
            warn("read $count bytes from handle $n") if $DEBUG;
            if (!defined $count) { # read error
                if ($!{EPIPE}) {
                    warn("read failure (gpg exited?) from handle $n: $!")
                        if $DEBUG;
                } else {
                    warn("read failure from handle $n: $!");
                }
                $reader->remove($rhandle);
                close $rhandle;
                next;
            }
            if ($count == 0) { # EOF
                warn("read done from handle $n") if $DEBUG;
                $reader->remove($rhandle);
                close $rhandle;
                next;
            }
        }
        for my $whandle (@$wready) {
            my $n = fileno($whandle);
            $woffset_of->{$whandle} = 0 if !exists $woffset_of->{$whandle};
            my $count = syswrite($whandle, $wbuf_of->{$whandle},
                                 $blocksize, $woffset_of->{$whandle});
            if (!defined $count) {
                if ($!{EPIPE}) { # write error
                    warn("write failure (gpg exited?) from handle $n: $!")
                        if $DEBUG;
                } else {
                    warn("write failure from handle $n: $!");
                }
                $writer->remove($whandle);
                close $whandle;
                next;
            }
            warn("wrote $count bytes to handle $n") if $DEBUG;
            $woffset_of->{$whandle} += $count;
            if ($woffset_of->{$whandle} >= length($wbuf_of->{$whandle})) {
                warn("write done to handle $n") if $DEBUG;
                $writer->remove($whandle);
                close $whandle;
                next;
            }
        }
    }
    return $rbuf_of;
}

sub update_icon {
    my $text_type = shift;

    $statusicon->set_from_stock("OpenPGP_Applet-${text_type}");
}

sub detect_received {
    my $clipboard = shift;

    update_icon(detect_text_type(get_validated_clipboard_text()));
}

sub handle_clipboard_owner_change {
    my $clipboard = shift;

    # Each time the applet is used, we receive an owner-change signal for an
    # empty PRIMARY clipboard. We don't want to swich from a valid clipboard to
    # an empty one.
    my $content = $clipboard->wait_for_text;
    if (defined $content && length $content) {
        set_freshest_clipboard($clipboard);
    }
    detect_received($clipboard);
}

# pixmaps base dir is provided by File::ShareDir;
sub make_icon_source {
    my $icon = shift;
    my $base = shift;
    my $ext  = shift;
    my $size = shift;

    my $pixmapdir = File::ShareDir::dist_dir('OpenPGP_Applet') . "/pixmaps";
    my $filename = "$pixmapdir/$base/$icon.$ext";
    my $source = Gtk3::IconSource->new();
    $source->set_filename($filename);
    $source->set_direction_wildcarded(1);
    $source->set_state_wildcarded(1);
    if (defined $size) {
        $source->set_size_wildcarded(0);
        $source->set_size(Gtk3::IconSize::from_name($size));
    } else {
        $source->set_size_wildcarded(1);
    }

    return $source;
}

sub init_icons_stock {
    my $factory = shift;

    $factory->add_default;
    my @stock_ids = map { "OpenPGP_Applet-$_" } qw{ message none signed text };

    foreach my $stock_id (@stock_ids) {
        my $iconset = Gtk3::IconSet->new();
        $iconset->add_source(make_icon_source($stock_id, "22x22",    "png", 'gtk-button'));
        $iconset->add_source(make_icon_source($stock_id, "22x22",    "png", 'gtk-menu'));
        $iconset->add_source(make_icon_source($stock_id, "22x22",    "png", 'gtk-large-toolbar'));
        $iconset->add_source(make_icon_source($stock_id, "22x22",    "png", 'gtk-small-toolbar'));
        $iconset->add_source(make_icon_source($stock_id, "48x48",    "png", 'gtk-dialog'));
        $iconset->add_source(make_icon_source($stock_id, "scalable", "svg"));
        $factory->add($stock_id, $iconset);
    }
}
