File Coverage

blib/lib/File/KeePass/Agent/unix.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package File::KeePass::Agent::unix;
2              
3             =head1 NAME
4              
5             File::KeePass::Agent::unix - platform specific utilities for Agent
6              
7             =cut
8              
9 1     1   5 use strict;
  1         3  
  1         37  
10 1     1   5 use warnings;
  1         3  
  1         34  
11 1     1   6 use Carp qw(croak);
  1         2  
  1         62  
12 1     1   438 use X11::Protocol;
  0            
  0            
13             use vars qw(%keysyms);
14             use X11::Keysyms qw(%keysyms); # part of X11::Protocol
15             use Term::ReadKey qw(ReadMode GetControlChars);
16              
17             my @end;
18             my $cntl;
19             END { $_->() for @end };
20              
21             my $raw;
22             sub _term_raw { ReadMode 'raw', \*STDOUT; $raw = 1 }
23             sub _term_restore { ReadMode 'restore', \*STDOUT; ($raw, my $prev) = (0, $raw); return $prev }
24              
25             sub init {
26             my $self = shift;
27             $self->{'no_menus'} = grep {$_ eq '--no_menus'} @ARGV;
28             }
29              
30             sub prompt_for_file {
31             my ($self, $args) = @_;
32             my $last_file = $self->read_config('last_file');
33             if ($last_file && $last_file =~ m{ ^./..(/.+)$ }x) {
34             $last_file = $self->home_dir . $1;
35             }
36             $last_file = '' if $last_file && grep {$_->[0] eq $last_file} @{ $self->keepass };
37             my $file = $self->_file_prompt("Choose the KeePass database file to open: ", $last_file);
38             if ($last_file
39             && $file
40             && $last_file ne $file
41             && -e $file
42             && !$args->{'no_save'}
43             && require IO::Prompt
44             && IO::Prompt::prompt("Save $file as default KeePass database? ", -yn, -d => 'y', -tty)) {
45             my $home = $self->home_dir;
46             my $copy = ($file =~ m{^\Q$home\E(/.+)$ }x) ? "./..$1" : $file;
47             $self->write_config(last_file => $copy);
48             }
49              
50             return $file;
51             }
52              
53             sub prompt_for_pass {
54             my ($self, $file) = @_;
55             require IO::Prompt;
56             return ''.IO::Prompt::prompt("Enter your master password for $file: ", -e => '*', -tty);
57             }
58              
59             sub prompt_for_keyfile {
60             my ($self, $file) = @_;
61             return $self->_file_prompt("Enter a master key filename (optional) for $file: ");
62             }
63              
64             sub _file_prompt {
65             my ($self, $msg, $def) = @_;
66             #$msg =~ s/(:\s*)$/ [$def]$1/ or $msg .= " [$def] " if $def;
67             require Term::ReadLine;
68              
69             my $was_raw = _term_restore();
70             my $out = Term::ReadLine->new('fkp')->readline($msg, $def);
71             _term_raw() if $was_raw;
72              
73             $out = '' if ! defined $out;
74             $out =~ s/\s+$//;
75             $out =~ s{~/}{$self->home_dir.'/'}e;
76             return length($out) ? $out : $def;
77             }
78              
79             sub home_dir {
80             my ($user,$passwd,$uid,$gid,$quota,$comment,$gcos,$home,$shell,$expire) = getpwuid($<);
81             return $home || croak "Couldn't find home dir for uid $<";
82             }
83              
84             sub _config_file {
85             my $self = shift;
86             my $home = $self->home_dir;
87             return "$home/.keepassx/config" if -e "$home/.keepassx/config";
88             return "$home/.config/keepassx/config.ini";
89             }
90              
91             my %map = (
92             last_file => 'LastFile',
93             pre_gap => 'AutoTypePreGap',
94             key_delay => 'AutoTypeKeyStrokeDelay',
95             );
96              
97             sub read_config {
98             my ($self, $key) = @_;
99             my $c = $self->{'config'} ||= $self->_ini_parse($self->_config_file);
100             if (! $key) {
101             return $c;
102             } elsif (my $_key = $map{$key}) {
103             return $c->{'Options'}->{$_key};
104             }
105             elsif ($key eq 'global_shortcut') {
106             return if ! defined(my $key = $c->{'Options'}->{'GlobalShortcutKey'});
107             my $mod = $c->{'Options'}->{'GlobalShortcutMods'};
108             return if !$mod || $mod !~ m{ ^\@Variant\( \\0\\0\\0\\r\\0\\0\\0\\x5\\x? ([a-f0-9]+) \)$ }x; # non-portable - qvariant \r should be QBitArray, \x5 is 5 bits
109             my $val = hex($1);
110             my $s = {
111             key => chr($key),
112             ctrl => $val & 0b00001 ? 1 : 0,
113             shift => $val & 0b00010 ? 1 : 0,
114             alt => $val & 0b00100 ? 1 : 0,
115             altgr => $val & 0b01000 ? 1 : 0,
116             win => $val & 0b10000 ? 1 : 0,
117             };
118             @{ $s }{qw(ctrl alt)} = (1, 1) if delete $s->{'altgr'};
119             return $s;
120             } else {
121             die "Unknown key $key";
122             }
123             }
124              
125             sub write_config {
126             my ($self, $key, $val) = @_;
127             my $c = $self->_ini_parse($self->_config_file, 1);
128             if (my $_key = $map{$key}) {
129             $c->{'Options'}->{$_key} = $val;
130             } else {
131             return;
132             }
133             $self->_ini_write($c, $self->_config_file);
134             delete $self->{'config'};
135             }
136              
137             sub x {
138             shift->{'x'} ||= do {
139             my $x = X11::Protocol->new;
140             $x->{'error_handler'} = sub { my ($x, $d) = @_; die $x->format_error_msg($d) };
141             $x;
142             };
143             }
144              
145             ###----------------------------------------------------------------###
146              
147             sub no_menus { shift->{'no_menus'} }
148              
149             sub main_loop {
150             my $self = shift;
151              
152             my $kdbs = $self->keepass;
153             die "No open databases.\n" if ! @$kdbs;
154             my @callbacks = $self->active_callbacks;
155             if ($self->no_menus) {
156             for my $pair (@$kdbs) {
157             my ($file, $kdb) = @$pair;
158             print "$file\n";
159             print $kdb->dump_groups({'group_title !' => 'Backup', 'title !' => 'Meta-Info'})
160             }
161             die "No key callbacks defined and menus are disabled. Exiting\n" if ! @callbacks;
162             }
163              
164             $self->_bind_global_keys(@callbacks);
165             $self->_listen;
166             }
167              
168             sub _unbind_global_keys {
169             my $self = shift;
170             my $x = $self->x;
171             foreach my $pair (@{ delete($self->{'_bound_keys'}) || [] }) {
172             my ($code, $mod) = @$pair;
173             $x->UngrabKey($code, $mod, $x->root);
174             }
175             }
176              
177             sub _bind_global_keys {
178             my ($self, @callbacks) = @_;
179             #my $ShiftMask = 1;
180             #my $LockMask = 2;
181             #my $ControlMask = 4;
182             my $Mod2Mask = 16; # 1 => 8, 2 => 16, 3 => 32, 4 => 64, 5 => 128
183              
184             $self->_unbind_global_keys;
185             push @end, sub { $self->_unbind_global_keys };
186              
187             my $cb_map = $self->{'global_cb_map'} = {};
188             my $x = $self->x;
189             $self->{'bound_msg'} = '';
190             foreach my $c (@callbacks) {
191             my ($shortcut, $s_name, $callback) = @$c;
192              
193             my $code = $self->keycode($shortcut->{'key'});
194             my $mod = 0;
195             foreach my $row ([ctrl => 'Control'], [shift => 'Shift'], [alt => 'Mod1'], [win => 'Mod4']) {
196             next if ! $shortcut->{$row->[0]};
197             $mod |= 2 ** $x->num('KeyMask', $row->[1]);
198             }
199             foreach my $MOD (
200             $mod,
201             $mod|$Mod2Mask,
202             #$mod|$LockMask,
203             #$mod|$Mod2Mask|$LockMask,
204             ) {
205             my $seq = eval { $x->GrabKey($code, $MOD, $x->root, 1, 'Asynchronous', 'Asynchronous') };
206             croak "The key binding ".$self->shortcut_name($shortcut)." appears to already be in use" if ! $seq;
207             $cb_map->{$code}->{$MOD} = $callback;
208             push @{ $self->{'_bound_keys'} }, [$code, $MOD];
209             }
210             my $msg = "Listening to ".$self->shortcut_name($shortcut)." for $s_name\n";
211             print $msg;
212             $self->{'bound_msg'} .= $msg;
213             }
214             }
215              
216             sub _listen {
217             my $self = shift;
218             my $x = $self->x;
219             $x->event_handler('queue');
220              
221             # allow for only looking at grabbed keys
222             if ($self->no_menus) {
223             $self->read_x_event while 1;
224             exit;
225             }
226              
227              
228             # in addition to grabbed keys show an interactive menu of the options
229             # listen to both the x protocol events as well as our local term
230             require IO::Select;
231              
232             my $in_fh = \*STDIN;
233             local $SIG{'INT'} = sub { _term_restore(); exit };
234             push @end, sub { _term_restore() };
235             _term_raw();
236              
237             my $x_fh = $x->{'connection'}->fh;
238             $x_fh->autoflush(1);
239             STDOUT->autoflush(1);
240              
241             my $sel = IO::Select->new($x_fh, $in_fh);
242              
243             # handle events as they occur
244             $self->_init_state(1);
245             my $i;
246             while (1) {
247             my ($fh) = $sel->can_read(10);
248             next if ! $fh;
249             if ($fh == $in_fh) {
250             $self->_handle_term_input($fh) || last;
251             } else {
252             $self->read_x_event;
253             }
254             }
255             }
256              
257             sub read_x_event {
258             my $self = shift;
259             my $cb_map = shift || $self->{'global_cb_map'} || die "No global callbacks initialized\n";
260             my $x = $self->x;
261             my %event = $x->next_event;
262             return if ($event{'name'} || '') ne 'KeyRelease';
263             my $code = $event{'detail'};
264             my $mod = $event{'state'};
265             my $callback = $cb_map->{$code}->{$mod} || return;
266             my ($wid) = $x->GetInputFocus;
267             my $orig = $wid;
268             my $title = eval { $self->wm_name($wid) };
269             while (!defined($title) || ! length($title)) {
270             last if $wid == $x->root;
271             my ($root, $parent) = $x->QueryTree($wid);
272             last if $parent == $wid;
273             $wid = $parent;
274             $title = eval { $self->wm_name($wid) };
275             }
276             if (!defined($title) || !length($title)) {
277             warn "Could not find window title for window id $orig\n";
278             return;
279             }
280             $event{'_window_id'} = $wid;
281             $event{'_window_id_orig'} = $orig;
282             $self->$callback($title, \%event);
283             }
284              
285             ###----------------------------------------------------------------###
286              
287             sub keymap {
288             my $self = shift;
289             return $self->{'keymap'} ||= do {
290             my $min = $self->x->{'min_keycode'};
291             my @map = $self->x->GetKeyboardMapping($min, $self->x->{'max_keycode'} - $min);
292             my %map;
293             my $req_sh = $self->{'requires_shift'} = {};
294             my %rev = reverse %keysyms;
295             foreach my $m (@map) {
296             my $code = $min++;
297             foreach my $pair ([$m->[0], 0], (($m->[1] && $m->[1] != $m->[0]) ? ([$m->[1], 1]) : ())) {
298             my ($sym, $shift) = @$pair;
299             my $name = $rev{$sym};
300             next if ! defined $name;
301             if (! $map{$name}) {
302             $map{$name} = $code;
303             $req_sh->{$name} = 1 if $shift;
304             }
305             my $chr = ($sym < 0xFF00) ? chr($sym) : ($sym <= 0xFFFF) ? chr(0xFF & $sym) : next;
306             if ($chr ne $name && !$map{$chr}) {
307             $map{$chr} = $code;
308             $req_sh->{$chr} = 1 if $shift;
309             }
310             }
311             }
312             $map{"\n"} = $map{"\r"}; # \n mapped to Linefeed - we want it to be Return
313             $req_sh->{"\n"} = $req_sh->{"\r"};
314             \%map;
315             };
316             }
317              
318             sub requires_shift {
319             my $self = shift;
320             $self->keymap;
321             return $self->{'requires_shift'};
322             }
323              
324             sub keycode {
325             my ($self, $key) = @_;
326             return $self->keymap->{$key};
327             }
328              
329             sub is_key_pressed {
330             my $self = shift;
331             my $key = shift || return;
332             my $keys = shift || $self->x->QueryKeymap;
333             my $code = $self->keycode($key) || return;
334             my $byte = substr($keys, $code/8, 1);
335             my $n = ord $byte;
336             my $on = $n & (1 << ($code % 8));
337             if ($self->requires_shift->{$key} && @_ <= 3) {
338             return if ! $self->is_key_pressed('Shift_L', $keys, 'norecurse');
339             }
340             return $on;
341             }
342              
343             sub are_keys_pressed {
344             my $self = shift;
345             my $keys = $self->x->QueryKeymap;
346             return grep { $self->is_key_pressed($_, $keys) } @_;
347             }
348              
349             ###----------------------------------------------------------------###
350              
351             sub attributes {
352             my ($self, $wid) = @_;
353             return {$self->x->GetWindowAttributes($wid)};
354             }
355              
356             sub property {
357             my ($self, $wid, $prop) = @_;
358             return '' if !defined($wid) || $wid =~ /\D/;
359             $prop = $self->x->atom($prop) if $prop !~ /^\d+$/;
360             my ($val) = $self->x->GetProperty($wid, $prop, 'AnyPropertyType', 0, 255, 0);
361             return $val;
362             }
363              
364             sub properties {
365             my ($self, $wid) = @_;
366             my $x = $self->x;
367             return {map {$x->atom_name($_) => $self->property($wid, $_)} $x->ListProperties($wid) };
368             }
369              
370             sub wm_name {
371             my ($self, $wid) = @_;
372             return $self->property($wid, 'WM_NAME');
373             }
374              
375             sub all_children {
376             my ($self, $wid, $cache, $level) = @_;
377             $cache ||= {};
378             $level ||= 0;
379             next if exists $cache->{$wid};
380             $cache->{$wid} = $level;
381             my ($root, $parent, @children) = $self->x->QueryTree($wid);
382             $self->all_children($_, $cache, $level + 1) for @children;
383             return $cache;
384             }
385              
386             ###----------------------------------------------------------------###
387              
388             sub send_key_press {
389             my ($self, $auto_type, $entry, $title, $event) = @_;
390             warn "Auto-Type: $entry->{'title'}\n" if ref($entry);
391              
392             my ($wid) = $self->x->GetInputFocus;
393              
394             # wait for all other keys to clear out before we begin to type
395             my $i = 0;
396             while (my @pressed = $self->are_keys_pressed(qw(Shift_L Shift_R Control_L Control_R Alt_L Alt_R Meta_L Meta_R Super_L Super_R Hyper_L Hyper_R Escape))) {
397             print "Waiting for @pressed\n" if 5 == (++$i % 40);
398             select(undef,undef,undef,.05)
399             }
400              
401             my $pre_gap = $self->read_config('pre_gap') * .001;
402             my $delay = $self->read_config('key_delay') * .001;
403             my $keymap = $self->keymap;
404             my $shift = $self->requires_shift;
405             select undef, undef, undef, $pre_gap if $pre_gap;
406             for my $key (split //, $auto_type) {
407             my ($_wid) = $self->x->GetInputFocus; # send the key stroke
408             if ($_wid != $wid) {
409             warn "Window changed. Aborted Auto-type.\n";
410             last;
411             }
412             my $code = $keymap->{$key};
413             my $state = $shift->{$key} || 0;
414             if (! defined $code) {
415             warn "Could not find code for $key\n";
416             next;
417             }
418             select undef, undef, undef, $delay if $delay;
419             $self->key_press($code, $state, $wid);
420             $self->key_release($code, $state, $wid);
421             }
422             return;
423             }
424              
425             sub key_press {
426             my ($self, $code, $state, $wid) = @_;
427             my $x = $self->x;
428             ($wid) = $self->x->GetInputFocus if ! $wid;
429             return $x->SendEvent($wid, 0, 0, $x->pack_event(
430             name => "KeyPress",
431             detail => $code,
432             time => 0,
433             root => $x->root,
434             event => $wid,
435             state => $state || 0,
436             same_screen => 1,
437             ));
438             }
439              
440             sub key_release {
441             my ($self, $code, $state, $wid) = @_;
442             my $x = $self->x;
443             ($wid) = $self->x->GetInputFocus if ! $wid;
444             return $x->SendEvent($wid, 0, 0, $x->pack_event(
445             name => "KeyRelease",
446             detail => $code,
447             time => 0,
448             root => $x->root,
449             event => $wid,
450             state => $state || 0,
451             same_screen => 1,
452             ));
453             }
454              
455             ###----------------------------------------------------------------###
456              
457             sub _handle_term_input {
458             my ($self, $fh) = @_;
459              
460             $cntl ||= {GetControlChars $fh};
461             $self->{'buffer'} = '' if ! defined $self->{'buffer'};
462             my $buf = delete $self->{'buffer'};
463             while (1) {
464             my @fh = IO::Select->new($fh)->can_read(0);
465             last if ! @fh;
466             my $chr = getc $fh;
467             exit if $chr eq $cntl->{'INTERRUPT'} || $chr eq $cntl->{'EOF'};
468             $buf .= $chr;
469             last if $chr eq "\n";
470             }
471             my $had_nl = chomp $buf;
472             print "\r$buf" if length $buf > 1;
473              
474             my $state = $self->{'state'} ||= [$self->_menu_groups];
475             my $cur = $state->[-1];
476             my ($text, $cb) = @$cur;
477             my $matches = grep {$_ =~ /^\Q$buf\E/} keys %$cb;
478             if (!$had_nl && $matches > 1) {
479             $self->{'buffer'} = $buf;
480             print "\r$buf" if length($buf) eq 1;# \r";
481             } elsif ($cb->{$buf}) {
482             print "\n" if !$had_nl;
483             my ($method, @args) = @{ $cb->{$buf} };
484             my $new = $self->$method(@args) || return 1;
485             push @$state, $new if $new->[0];
486             } elsif (length($buf) && $buf ne "\e") {
487             print "\n" if !$had_nl;
488             print "Unknown option ($buf)\n";
489             } else {
490             pop @$state if @$state > 1;
491             print $state->[-1]->[0];
492             }
493              
494             return 1;
495             }
496              
497             sub _init_state {
498             my ($self, $first_time) = @_;
499             $self->_bind_global_keys($self->active_callbacks) if !$first_time; # unbinds previous ones
500             my $state = $self->{'state'} = [$self->_menu_groups];
501             print $state->[-1]->[0];
502             }
503              
504             my @a2z = ('a'..'z', 0..9);
505             sub _a2z {
506             my $i = shift;
507             return $a2z[$i % @a2z] x (1 + ($i / @a2z));
508             }
509              
510             sub _close_file {
511             my ($self, $file) = @_;
512             $self->unload_keepass($file);
513             $self->_init_state;
514             return [];
515             }
516              
517             sub _clear {
518             return if shift->{'no_clear'};
519             return "\e[H\e[2J";
520             }
521              
522             sub _menu_groups {
523             my $self = shift;
524              
525             my $t = $self->_clear."\n";
526             my $i = 0;
527             my $cb = {};
528             foreach my $pair (@{ $self->keepass }) {
529             my ($file, $kdb) = @$pair;
530             $t .= " File: $file\n";
531             foreach my $g ($kdb->find_groups) {
532             my $indent = ' ' x $g->{'level'};
533             my $key = _a2z($i++);
534             $cb->{$key} = ['_menu_entries', $file, $g->{'id'}];
535             $t .= " ($key) $indent$g->{'title'}\n";
536             }
537             }
538              
539             $t .= "\n";
540             $t .= " (+) Open another keepass database\n";
541             $t .= " (-) Close a keepass database\n" if @{ $self->keepass };
542             $cb->{'+'} = ['_action_open'];
543             $cb->{'-'} = ['_action_close'];
544              
545             $t .= "\n".delete($self->{'bound_msg'}) if $self->{'bound_msg'};
546             return [$t, $cb];
547             }
548              
549             sub _action_open {
550             my $self = shift;
551             print "\n";
552             my $file = $self->prompt_for_file({no_save => 1});
553             if (!$file) {
554             print "No file specified.\n";
555             return [];
556             } elsif (!-e $file) {
557             print "File \"$file\" does not exist.\n";
558             return [];
559             } else {
560             my $k = $self->_prompt_for_pass_and_key($file);
561             print "Failed to open file $file\n";
562             $self->_init_state if $k;
563             }
564             return [];
565             }
566              
567             sub _action_close {
568             my $self = shift;
569             print "\n Close file\n";
570             my $i = 0;
571             my $cb = {};
572             my $t = '';
573             for my $file (map {$_->[0]} @{ $self->keepass }) {
574             my $key = _a2z($i++);
575             $cb->{$key} = ['_close_file', $file];
576             $t .= " ($key) $file\n";
577             }
578             print $t;
579             return [$t, $cb];
580             }
581              
582             sub _menu_entries {
583             my ($self, $file, $gid) = @_;
584             my ($kdb) = map {$_->[1]} grep {$_->[0] eq $file} @{ $self->keepass };
585             my $g = $kdb->find_group({id => $gid}) || do { print "\nNo such matching gid ($gid) in file ($file)\n\n"; return };
586             local $g->{'groups'}; # don't recurse while looking for entries since we are already flat
587             my @E = $kdb->find_entries({}, [$g]);
588             if (! @E) {
589             print "\nNo group entries in $g->{'title'}\n\n";
590             return;
591             }
592             my $t = $self->_clear."\n File: $file\n";
593             $t .= " Group: $g->{'title'}\n";
594              
595             my $i = 0;
596             my $cb = {};
597             my @e;
598             my $max = 0;
599             for my $e (@E) {
600             my $key = _a2z($i++);
601             $cb->{$key} = ['_menu_entry', $file, $e->{'id'}, $gid];
602             push @e, " ($key) $e->{'title'}";
603             $max = length($e[-1]) if length($e[-1]) > $max;
604             }
605              
606             my ($W, $H) = eval { Term::ReadKey::GetTerminalSize(\*STDOUT) };
607             my $cols = int($W / ($max || 1));
608             my $rows = @e / $cols; $rows = int(1 + $rows) if int($rows) != $rows;
609             $rows = 8 if $rows < 8;
610             my @row;
611             $row[$_%$rows]->[$_/$rows] = $e[$_] for 0 .. @e;
612             $t .= sprintf("%-${max}s"x@$_, @$_)."\n" for @row;
613             print $t;
614             return [$t, $cb];
615             }
616              
617             sub _menu_entry {
618             my ($self, $file, $eid, $gid, $action, $extra) = @_;
619             my ($kdb) = map {$_->[1]} grep {$_->[0] eq $file} @{ $self->keepass };
620             my $e = $kdb->find_entry({id => $eid}) || do { print "\nNo such matching eid ($eid) in file ($file)\n\n"; return };
621             my $g = $kdb->find_group({id => $gid}) || do { print "\nNo such matching gid ($gid) in file ($file)\n\n"; return };
622              
623             my $cb = {};
624             my $t = "\n File: $file\n";
625             $t .= " Group: $g->{'title'}\n";
626             $t .= " Entry: $e->{'title'}\n";
627              
628             $cb->{'i'} = ['_menu_entry', $file, $e->{'id'}, $gid, 'info'];
629             $cb->{'c'} = ['_menu_entry', $file, $e->{'id'}, $gid, 'comment'];
630             $cb->{'p'} = ['_menu_entry', $file, $e->{'id'}, $gid, 'print_pass'];
631             $cb->{'a'} = ['_menu_entry', $file, $e->{'id'}, $gid, 'auto_type'];
632             $cb->{'1'} = ['_menu_entry', $file, $e->{'id'}, $gid, 'copy', 'password'];
633             $cb->{'2'} = ['_menu_entry', $file, $e->{'id'}, $gid, 'copy', 'username'];
634             $cb->{'3'} = ['_menu_entry', $file, $e->{'id'}, $gid, 'copy', 'url'];
635             $cb->{'4'} = ['_menu_entry', $file, $e->{'id'}, $gid, 'copy', 'title'];
636             $cb->{'5'} = ['_menu_entry', $file, $e->{'id'}, $gid, 'copy', 'comment'];
637             $t .= " (i) Show entry information\n";
638             $t .= " (c) Show entry comment\n";
639             $t .= " (p) Print password\n";
640             $t .= " (a) Run Auto-Type in 5 seconds\n";
641             $t .= " (1) Copy password to clipboard\n";
642             $t .= " (2) Copy username to clipboard\n";
643             $t .= " (3) Copy url to clipboard\n";
644             $t .= " (4) Copy title to clipboard\n";
645             $t .= " (5) Copy comment to clipboard\n";
646             my $i = 6;
647             for my $key (sort keys %{ $e->{'strings'} || {} }) {
648             my $k = $i++;
649             $cb->{$k} = ['_menu_entry', $file, $e->{'id'}, $gid, 'copy', $key];
650             $t .= " ($k) Copy string \"$key\" to clipboard\n";
651             }
652             for my $key (sort keys %{ $e->{'binary'} || {} }) {
653             my $k = $i++;
654             $cb->{$k} = ['_menu_entry', $file, $e->{'id'}, $gid, 'save', $key];
655             $t .= " ($k) Save binary \"$key\" as...\n";
656             }
657              
658             if (!$action) {
659             print $self->_clear.$t;
660             return [$t, $cb];
661             }
662              
663             if ($action eq 'info') {
664             foreach my $k (sort keys %$e) {
665             next if $k eq 'comment' || $k eq 'comment';
666             my $val = $e->{$k};
667             if (ref($val) eq 'ARRAY') {
668             next if $k eq 'history' && !@$val;
669             $val = "(Previous versions: ".scalar(@$val).")" if $k eq 'history';
670             $val = join '', map {"\n \"$_->{'window'}\" --> \"$_->{'keys'}\""} @$val if $k eq 'auto_type';
671             } elsif (ref($val) eq 'HASH') {
672             next if $k eq 'binary' && ! scalar keys %$val;
673             $val = join '', map {"\n \"$_\" (".length($val->{$_})." bytes)"} sort keys %$val if $k eq 'binary';
674             $val = join '', map {"\n \"$_\" = \"$val->{$_}\""} sort keys %$val if $k eq 'strings' || $k eq 'protected';
675             }
676             print " $k: ".(defined($val) ? $val : "(null)")."\n";
677             }
678             } elsif ($action eq 'comment') {
679             print "-------------------\n";
680             if (! defined $e->{'comment'}) {
681             print "--No comment--\n";
682             } elsif (length $e->{'comment'}) {
683             print "--Empty comment--\n";
684             } else {
685             print $e->{'comment'};
686             print "\n--No newline--\n" if $e->{'comment'} !~ /\n$/;
687             }
688             } elsif ($action eq 'print_pass') {
689             my $pass = $kdb->locked_entry_password($e);
690             if (!defined $pass) {
691             print "--No password defined--\n";
692             } elsif (!length $pass) {
693             print "--Zero length password--\n";
694             } else {
695             print "$pass\n";
696             }
697             } elsif ($action eq 'auto_type') {
698             my $at = $e->{'auto_type'} || [];
699             if (!@$at || !defined($at->[0]->{'keys'}) || !length($at->[0]->{'keys'})) {
700             print "--No Auto-Type entry found for entry (defaulting to {PASSWORD}{ENTER})--\n";
701             $at = [{keys => '{PASSWORD}{ENTER}'}];
702             } elsif (@$at > 1) {
703             print "--Multiple Auto-Type entries found in comment - using the first one--\n";
704             }
705             my $keys = $at->[0]->{'keys'};
706             local $| = 1;
707             print "\n";
708             require IO::Select;
709             my $sel = IO::Select->new(\*STDIN);
710             for (reverse 1 .. 5) {
711             print "\rRunning Auto-Type in $_... (any key to cancel)";
712             my @fh = $sel->can_read(1);
713             if (@fh) {
714             read $fh[0], my $txt, 1;
715             print $self->_clear.$t."\n\nAuto-type cancelled\n";
716             return [];
717             }
718             }
719             my ($wid) = $self->x->GetInputFocus;
720             my $title = eval { $self->wm_name($wid) };
721              
722             print "\rSending Auto-Type to window: $title \n";
723              
724             $self->do_auto_type({
725             auto_type => $keys,
726             file => $file,
727             entry => $e,
728             }, $title, undef);
729             } elsif ($action eq 'copy') {
730             my $data = ($extra eq 'password') ? $kdb->locked_entry_password($e) : exists($e->{$extra}) ? $e->{$extra} : $e->{'strings'}->{$extra};
731             $data = '' if ! defined $data;
732             $self->_copy_to_clipboard($data) || return;
733             print "Sent $extra to clipboard\n";
734             print "--Zero length $extra--\n" if ! length $data;
735             } elsif ($action eq 'save') {
736             if (my $file = $self->_file_prompt("Save file \"$extra\" as: ", $extra)) {
737             if (open my $fh, ">", $file) {
738             binmode $fh;
739             print $fh $e->{'binary'}->{$extra};
740             close $fh;
741             print "Saved \"$extra\" as \"$file\"\n";
742             } else {
743             print "Could not open $file for writing: $!\n";
744             }
745             } else {
746             print "File not saved\n";
747             }
748             } else {
749             print "--Unknown action $action--\n";
750             }
751             return [];
752             }
753              
754             sub _copy_to_clipboard {
755             my ($self, $data) = @_;
756             if (my $klip = eval {
757             require Net::DBus;
758             my $bus = Net::DBus->find;
759             my $obj = $bus->get_service("org.freedesktop.DBus")->get_object("/org/freedesktop/DBus");
760             my %h = map {$_ => 1} @{ $obj->ListNames };
761             die "No klipper service found" unless $h{'org.kde.klipper'};
762             return $bus->get_service('org.kde.klipper')->get_object('/klipper');
763             }) {
764             $klip->setClipboardContents($data);
765             return 1;
766             } elsif (-x '/usr/bin/xclip' && open(my $prog, '|-', '/usr/bin/xclip', '-selection', 'clipboard')) {
767             print $prog $data;
768             close $prog;
769             } else {
770             print "--No current clipboard service available\n";
771             return;
772             }
773             }
774              
775             ###----------------------------------------------------------------###
776              
777             sub _ini_parse { # ick - my own config.ini reader - too bad the main cpan entries are overbloat
778             my ($self, $file, $order) = @_;
779             open my $fh, '<', $file or return {};
780             my $block = '';
781             my $c = {};
782             while (defined(my $line = <$fh>)) {
783             $line =~ s/^\s+//;
784             $line =~ s/\s+$//;
785             if ($line =~ /^ \[\s* (.*?) \s*\] $/x) {
786             $block = $1;
787             push @{ $c->{"\eorder\e"} }, $block if $order;
788             next;
789             } elsif (!length $line || $line =~ /^[;\#]/) {
790             push @{ $c->{$block}->{"\eorder\e"} }, \$line if $order;
791             next;
792             }
793             my ($key, $val) = split /\s*=\s*/, $line, 2;
794             $c->{$block}->{$key} = $val;
795             push @{ $c->{$block}->{"\eorder\e"} }, $key if $order;
796             }
797             return $c;
798             }
799              
800             sub _ini_write {
801             my ($self, $c, $file) = @_;
802             open my $fh, "+<", $file or die "Could not open file $file for writing: $!";
803             for my $block (@{ $c->{"\eorder\e"} || [sort keys %$c] }) {
804             print $fh "[$block]\n" if length $block;
805             my $ref = $c->{$block} || {};
806             for my $key (@{ $ref->{"\eorder\e"} || [sort keys %$ref] }) {
807             if (ref($key) eq 'SCALAR') {
808             print $fh $$key,"\n";
809             } else {
810             print $fh "$key=".(defined($ref->{$key}) ? $ref->{$key} : '')."\n";
811             }
812             }
813             }
814             truncate $fh, tell($fh);
815             close $fh;
816             }
817              
818              
819             =head1 DESCRIPTION
820              
821             This module provides unix based support for the File::KeePassAgent. It should
822             work for anything using an X server. It should not normally be used on its own.
823              
824             =head1 FKPA METHODS
825              
826             The following methods must be provided by an FKPA OS variant.
827              
828             =over 4
829              
830             =item C
831              
832             Takes the name of a key to read from the configuration file. This method reads from
833             $HOME/.config/keepassx/config.ini.
834              
835             =item C
836              
837             Requests the name of a keepass database to open.
838              
839             =item C
840              
841             Requests for the password to open the choosen keepass database.
842             It is passed the name of the file being opened.
843              
844             =item C
845              
846             Takes a list of arrayrefs. Each arrayref should
847             contain a shortcut key description hashref and a callback.
848              
849             $self->grab_global_keys([{ctrl => 1, shift => 1, alt => 1, key => "c"}, sub { print "Got here" }]);
850              
851             The callback will be called as a method of the Agent object. It will
852             be passed the current active window title and the generating event.
853              
854             $self->$callback($window_title, \%event);
855              
856             This method use X11::Protocol to bind the shortcuts, then listens for the events to happen.
857              
858             =item C
859              
860             Takes an auto-type string, the keepass entry that generated the request,
861             the current active window title, and the generating event.
862              
863             This method uses X11::GUITest to "type" the chosen text to the X server.
864              
865             =back
866              
867             =head1 OTHER METHODS
868              
869             These methods are not directly used by the FKPA api.
870              
871             =over 4
872              
873             =item C
874              
875             Used by read_config to find the users home directory.
876              
877             =item C
878              
879             Returns an X11::Protocol object
880              
881             =item C
882              
883             Returns the keymap in use by the X server.
884              
885             =item C
886              
887             Returns the keysym id used by the X server.
888              
889             =item C
890              
891             Takes a key - returns the appropriate key code for use in grab_global_keys
892              
893             =item C
894              
895             Returns true if the key is currently pressed. Most useful for items
896             like Control_L, Shift_L, or Alt_L.
897              
898             =item C
899              
900             Takes an array of key names and returns which ones are currently
901             pressed. It has a little bit of caching as part of the process of
902             calling is_key_pressed. Returns any of the key names that are pressed.
903              
904             =item C
905              
906             Takes an X window id - returns all of the attributes for the window.
907              
908             =item C
909              
910             Takes an X window id and a property name. Returns the current value of that property.
911              
912             =item C
913              
914             Takes an X window id - returns all of the properties for the window.
915              
916             =item C
917              
918             Takes an X window id - returns its window manager name.
919              
920             =item C
921              
922             Returns all decended children of an X window.
923              
924             =back
925              
926             =head1 AUTHOR
927              
928             Paul Seamons
929              
930             =head1 LICENSE
931              
932             This module may be distributed under the same terms as Perl itself.
933              
934             =cut
935              
936             1;