#!/usr/bin/perl

=head1 NAME

OpenPGP_Applet - GNOME applet for OpenPGP text encryption

=head1 VERSION

Version 0.9.1

=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.

'xclip' (http://xclip.sourceforge.net) is used to get input from clipboard.


=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 = 0.9.1;

use Glib qw{TRUE FALSE};
use Gtk2 qw{-init};
use Gtk2::Gdk::Keysyms;
use Gtk2::SimpleList;

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

use Locale::TextDomain ("OpenPGP_Applet");
use POSIX;
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();
my $codeset       = langinfo(CODESET());
my $encoding      = find_encoding($codeset);
my $main_window   = Gtk2::Window->new();
my $icon_factory  = Gtk2::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());
Gtk2->main;


=head1 FUNCTIONS

=cut

sub all_clipboards {
    map {
        Gtk2::Clipboard->get($_)
    } (
        Gtk2::Gdk->SELECTION_CLIPBOARD,
        Gtk2::Gdk->SELECTION_PRIMARY
    );
}

{
    my $freshest_clipboard;

    sub init_freshest_clipboard {
        $freshest_clipboard = Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_CLIPBOARD);
    }

    sub freshest_clipboard {
        return $freshest_clipboard;
    }

    sub set_freshest_clipboard {
        $freshest_clipboard = shift;
    }
}

sub app_exit {
    my $parent = shift;
    my $dialog = Gtk2::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');
    Gtk2->main_quit if ($dialog->run eq 'yes');

    $dialog->destroy;
}

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

    my $menu   = Gtk2::Menu->new;
    my $mexit  = Gtk2::MenuItem->new($encoding->decode(__("Exit")));
    $mexit->signal_connect('activate' => sub { app_exit($main_window); });
    my $mabout = Gtk2::MenuItem->new($encoding->decode(__("About")));
    $mabout->signal_connect('activate' => sub { Gtk2->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(Gtk2::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;
        my $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 = Gtk2::Menu->new;

    my $text_type = detect_text_type(get_validated_clipboard_text());

    if ($text_type eq 'text' or $text_type eq 'none') {
        my $msymencrypt    = Gtk2::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    = Gtk2::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 = Gtk2::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 = Gtk2::MenuItem->new_with_mnemonic($encoding->decode(__("_Manage Keys")));
    $mmanage->signal_connect('activate' => sub { manage_keys(); });
    $action_menu->append($mmanage);

    my $mtexteditor = Gtk2::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();
    # 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);

    # Note: according to the GTK documentation, the set_text method
    # is supposed to need input encoded in UTF-8. But it seems like the Perl
    # bindings encode it, and we need to pass a string of chars instead of bytes.
    foreach (all_clipboards()) {
        $_->set_text($text);
    }
    # GTK fails setting the primary selection above, so let's use xclip :/
    # xclip needs encoded text.
    open(my $xclip, '|-', 'xclip') or die "Error opening pipe to xclip";
    print $xclip $encoded_text or die "Error copying data to X clipboard";
    close $xclip or die "Error closing pipe to xclip";
}

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());
    # Gtk2::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 = Gtk2::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 Gtk2::TreeView instead of Gtk2::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 $path = $list->get_path_at_pos($x, $y);
        return FALSE unless defined $path;
        my $row = ($path->get_indices)[0];
        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 = Gtk2::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 = Gtk2::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 = Gtk2::ComboBox->new_with_model($sorted_list);
    my $renderer = Gtk2::CellRendererText->new();
    $combo->pack_start($renderer, FALSE);
    $combo->add_attribute($renderer, 'text', COMBO_NAME);
    $renderer = Gtk2::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 = Gtk2::Label->new(
        $encoding->decode(__("Select recipients:")));

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

    my $hide_recipients_checkbox = Gtk2::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 = Gtk2::Label->new(
        $encoding->decode(__("Sign message as:")));

    my $priv_key_combo = make_priv_key_combo($priv_keys_ref);

    my $dialog = Gtk2::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 = Gtk2::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 == $Gtk2::Gdk::Keysyms{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;

    # We assume the sender/recipient uses the same charset as us :/
    # PGP/MIME was invented for a reason.
    print $in_h $encoding->encode($text);
    close $in_h;

    my ($err, $out) = read_err_out($err_h, $out_h);
    my @raw_stderr = @{$err};
    my @raw_stdout = @{$out};

    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 = Gtk2::MessageDialog->new(
        $parent, 'destroy-with-parent', 'error', 'ok',
        $title
    );
    $dialog->format_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 = Gtk2::MessageDialog->new(
        $parent, 'destroy-with-parent', 'question', 'yes-no', $title);
    $dialog->format_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 = Gtk2::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->format_secondary_text(sprintf($encoding->decode(
        __("Output of GnuPG:")
    )));

    my $msg_area = $dialog->get_content_area;

    my $outbuf = Gtk2::TextBuffer->new();
    $outbuf->set_text($std_out);
    my $text_desc = Pango::FontDescription->new;
    $text_desc->set_family('Monospace');
    my $textview_out = Gtk2::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 = Gtk2::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 = Gtk2::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 = Gtk2::TextBuffer->new();
        $std_err_buf->set_text($std_err);
        my $textview_err = Gtk2::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 = Gtk2::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;
}

# Read stdout and stderr at the same time, one line at a time, to
# avoid dead-locking due to one of the buffers being full.
sub read_err_out {
    my $err_h = shift;
    my $out_h = shift;

    my $err   = [];
    my $out   = [];

    while (1) {
        my $err_l = <$err_h>;
        my $out_l = <$out_h>;
        push @{$err}, $err_l if defined $err_l;
        push @{$out}, $out_l if defined $out_l;
        last unless ($err_l || $out_l);
    }
    close $err_h;
    close $out_h;

    return ($err, $out);
}

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 = Gtk2::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($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 = Gtk2::IconSet->new();
        $iconset->add_source(make_icon_source($stock_id, "22x22",    "png", 'button'));
        $iconset->add_source(make_icon_source($stock_id, "22x22",    "png", 'menu'));
        $iconset->add_source(make_icon_source($stock_id, "22x22",    "png", 'large-toolbar'));
        $iconset->add_source(make_icon_source($stock_id, "22x22",    "png", 'small-toolbar'));
        $iconset->add_source(make_icon_source($stock_id, "48x48",    "png", 'dialog'));
        $iconset->add_source(make_icon_source($stock_id, "scalable", "svg"));
        $factory->add($stock_id, $iconset);
    }
}
