File Coverage

blib/lib/File/KDBX/Entry.pm
Criterion Covered Total %
statement 403 447 90.1
branch 158 252 62.7
condition 117 192 60.9
subroutine 82 86 95.3
pod 48 50 96.0
total 808 1027 78.6


line stmt bran cond sub pod time code
1             package File::KDBX::Entry;
2             # ABSTRACT: A KDBX database entry
3              
4 10     10   363399 use warnings;
  10         20  
  10         300  
5 10     10   51 use strict;
  10         22  
  10         262  
6              
7 10     10   2716 use Crypt::Misc 0.049 qw(decode_b64 encode_b32r);
  10         39840  
  10         616  
8 10     10   1865 use Devel::GlobalDestruction;
  10         2299  
  10         67  
9 10     10   594 use Encode qw(encode);
  10         17  
  10         359  
10 10     10   123 use File::KDBX::Constants qw(:history :icon);
  10         22  
  10         3792  
11 10     10   69 use File::KDBX::Error;
  10         18  
  10         487  
12 10     10   59 use File::KDBX::Util qw(:assert :class :coercion :erase :function :uri generate_uuid load_optional);
  10         19  
  10         1727  
13 10     10   2366 use Hash::Util::FieldHash;
  10         3900  
  10         361  
14 10     10   60 use List::Util qw(any first sum0);
  10         15  
  10         553  
15 10     10   52 use Ref::Util qw(is_coderef is_hashref is_plain_hashref);
  10         15  
  10         1577  
16 10     10   65 use Scalar::Util qw(blessed looks_like_number);
  10         17  
  10         403  
17 10     10   5405 use Storable qw(dclone);
  10         29771  
  10         597  
18 10     10   68 use Time::Piece 1.33;
  10         178  
  10         66  
19 10     10   704 use boolean;
  10         29  
  10         62  
20 10     10   541 use namespace::clean;
  10         17  
  10         73  
21              
22             extends 'File::KDBX::Object';
23              
24             our $VERSION = '0.904'; # VERSION
25              
26             my $PLACEHOLDER_MAX_DEPTH = 10;
27             my %PLACEHOLDERS;
28             my %STANDARD_STRINGS = map { $_ => 1 } qw(Title UserName Password URL Notes);
29              
30              
31             sub uuid {
32 449     449 1 569 my $self = shift;
33 449 100 100     1349 if (@_ || !defined $self->{uuid}) {
34 98 100       304 my %args = @_ % 2 == 1 ? (uuid => shift, @_) : @_;
35 98         153 my $old_uuid = $self->{uuid};
36 98   66     336 my $uuid = $self->{uuid} = delete $args{uuid} // generate_uuid;
37 98         155 for my $entry (@{$self->history}) {
  98         223  
38 6         14 $entry->{uuid} = $uuid;
39             }
40 98 100 66     348 $self->_signal('uuid.changed', $uuid, $old_uuid) if defined $old_uuid && $self->is_current;
41             }
42 449         1077 $self->{uuid};
43 182 50   182 1 564 }
44 182 100   98 1 608  
  98 50       294  
45 182 100 66 84 1 1452 # has uuid => sub { generate_uuid(printable => 1) };
  98 50       223  
  84         231  
46 98 50 100 84 1 451 has icon_id => ICON_PASSWORD, coerce => \&to_icon_constant;
  84 50       173  
  84         265  
47 84 50 33 84 1 481 has custom_icon_uuid => undef, coerce => \&to_uuid;
  84 50       174  
  84         255  
48 84 50 33 87 1 401 has foreground_color => '', coerce => \&to_string;
  84 50       183  
  87         240  
49 84 50 33 511 0 358 has background_color => '', coerce => \&to_string;
  87 50       207  
  511         1171  
50 87 50 66 113 1 432 has override_url => '', coerce => \&to_string;
  511 50       873  
  113         324  
51 511 100 100 138 1 2117 has tags => '', coerce => \&to_string;
  113 50       269  
  138         509  
52 113 100 100 134 1 569 has auto_type => {};
  138 50       286  
  134         762  
53 138 50 66 134 1 593 has previous_parent_group => undef, coerce => \&to_uuid;
  134 50       265  
  134         1700  
54 134 50 50 720 0 443 has quality_check => true, coerce => \&to_bool;
  134 50       278  
  720         1735  
55 134 50 100     691 has strings => {};
  720         1123  
56 720   100     3071 has binaries => {};
57             has times => {};
58 100 50   100 1 334 # has custom_data => {};
59 100 100   87 1 236 # has history => [];
  87 50       8157  
60 100 100 100 92 1 184  
  87 50       239  
  92         5008  
61 87 100 100 84 1 184 has last_modification_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  92 50       233  
  84         4944  
62 92 50 100 84 1 205 has creation_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  84 50       212  
  84         4660  
63 84 50 50 84 1 164 has last_access_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  84 50       204  
  84         607  
64 84 50 33 85 1 165 has expiry_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  84 50       182  
  85         251  
65 84 100 33     153 has expires => false, store => 'times', coerce => \&to_bool;
  85         188  
66 85   100     154 has usage_count => 0, store => 'times', coerce => \&to_number;
67 84 50   84 1 4737 has location_changed => sub { gmtime }, store => 'times', coerce => \&to_time;
68 84 50       221  
69 84 50 33 84 1 200 # has 'auto_type.auto_type_enabled' => true, coerce => \&to_bool;
  84         275  
70 84 50       218 has 'auto_type_obfuscation' => 0, path => 'auto_type.data_transfer_obfuscation',
71 84 50 33 87 1 171 coerce => \&to_number;
  87         264  
72 87 50       175 has 'auto_type_default_sequence' => '{USERNAME}{TAB}{PASSWORD}{ENTER}',
73 87   100     170 path => 'auto_type.default_sequence', coerce => \&to_string;
74             has 'auto_type_associations' => [], path => 'auto_type.associations';
75              
76             my %ATTRS_STRINGS = (
77             title => 'Title',
78             username => 'UserName',
79             password => 'Password',
80             url => 'URL',
81             notes => 'Notes',
82             );
83             while (my ($attr, $string_key) = each %ATTRS_STRINGS) {
84 10     10   15962 no strict 'refs'; ## no critic (ProhibitNoStrict)
  10         19  
  10         41312  
85 479     479   6360 *{$attr} = sub { shift->string_value($string_key, @_) };
86 323     323   1442 *{"expand_${attr}"} = sub { shift->expand_string_value($string_key, @_) };
87             }
88              
89             my @ATTRS = qw(uuid custom_data history auto_type_enabled);
90             sub _set_nonlazy_attributes {
91 84     84   128 my $self = shift;
92 84         405 $self->$_ for @ATTRS, keys %ATTRS_STRINGS, list_attributes(ref $self);
93             }
94              
95             sub init {
96 104     104 1 163 my $self = shift;
97 104         218 my %args = @_;
98              
99 104         363 while (my ($key, $val) = each %args) {
100 156 100       539 if (my $method = $self->can($key)) {
101 154         312 $self->$method($val);
102             }
103             else {
104 2         5 $self->string($key => $val);
105             }
106             }
107              
108 104         260 return $self;
109             }
110              
111             ##############################################################################
112              
113              
114             sub string {
115 1263     1263 1 1538 my $self = shift;
116 1263 50       3660 my %args = @_ == 2 ? (key => shift, value => shift)
    100          
117             : @_ % 2 == 1 ? (key => shift, @_) : @_;
118              
119 1263 0 33     2349 if (!defined $args{key} && !defined $args{value}) {
120 0         0 my %standard = (value => 1, protect => 1);
121 0         0 my @other_keys = grep { !$standard{$_} } keys %args;
  0         0  
122 0 0       0 if (@other_keys == 1) {
123 0         0 my $key = $args{key} = $other_keys[0];
124 0         0 $args{value} = delete $args{$key};
125             }
126             }
127              
128 1263 50       2375 my $key = delete $args{key} or throw 'Must provide a string key to access';
129              
130 1263 50       3413 return $self->{strings}{$key} = $args{value} if is_plain_hashref($args{value});
131              
132 1263         3246 while (my ($field, $value) = each %args) {
133 145         571 $self->{strings}{$key}{$field} = $value;
134             }
135              
136             # Auto-vivify the standard strings.
137 1263 100       2282 if ($STANDARD_STRINGS{$key}) {
138 809 100 100     2448 return $self->{strings}{$key} //= {value => '', $self->_protect($key) ? (protect => true) : ()};
139             }
140 454         1323 return $self->{strings}{$key};
141             }
142              
143             ### Get whether or not a standard string is configured to be protected
144             sub _protect {
145 348     348   773 my $self = shift;
146 348         437 my $key = shift;
147 348 50       629 return false if !$STANDARD_STRINGS{$key};
148 348 100       488 if (my $kdbx = eval { $self->kdbx }) {
  348         1266  
149 89         179 my $protect = $kdbx->memory_protection($key);
150 89 50       283 return $protect if defined $protect;
151             }
152 259         1452 return $key eq 'Password';
153             }
154              
155              
156             sub string_value {
157 875     875 1 14559 my $self = shift;
158 875   100     1353 my $string = $self->string(@_) // return undef;
159 599         3033 return $string->{value};
160             }
161              
162              
163             sub _expand_placeholder {
164 43     43   62 my $self = shift;
165 43         57 my $placeholder = shift;
166 43         62 my $arg = shift;
167              
168 43         148 require File::KDBX;
169              
170 43         53 my $placeholder_key = $placeholder;
171 43 100       83 if (defined $arg) {
172 12 50       45 $placeholder_key = $File::KDBX::PLACEHOLDERS{"${placeholder}:${arg}"} ? "${placeholder}:${arg}"
173             : "${placeholder}:";
174             }
175 43 100       94 return if !defined $File::KDBX::PLACEHOLDERS{$placeholder_key};
176              
177 42         116 my $local_key = join('/', Hash::Util::FieldHash::id($self), $placeholder_key);
178 42   66     93 local $PLACEHOLDERS{$local_key} = my $handler = $PLACEHOLDERS{$local_key} // do {
179 23 50       45 my $handler = $File::KDBX::PLACEHOLDERS{$placeholder_key} or next;
180             memoize recurse_limit($handler, $PLACEHOLDER_MAX_DEPTH, sub {
181 1     1   9 alert "Detected deep recursion while expanding $placeholder placeholder",
182             placeholder => $placeholder;
183 1         31 return; # undef
184 23         104 });
185             };
186              
187 42         106 return $handler->($self, $arg, $placeholder);
188             }
189              
190             sub _expand_string {
191 323     323   487 my $self = shift;
192 323         396 my $str = shift;
193              
194 323         1069 my $expand = memoize $self->can('_expand_placeholder'), $self;
195              
196             # placeholders (including field references):
197 323   66     830 $str =~ s!\{([^:\}]+)(?::([^\}]*))?\}!$expand->(uc($1), $2, @_) // $&!egi;
  44         194  
198              
199             # environment variables (alt syntax):
200 323         1842 my $vars = join('|', map { quotemeta($_) } keys %ENV);
  10984         14914  
201 323   33     3412 $str =~ s!\%($vars)\%!$expand->(ENV => $1, @_) // $&!eg;
  2         10  
202              
203 323         1965 return $str;
204             }
205              
206             sub expand_string_value {
207 323     323 1 390 my $self = shift;
208 323   50     600 my $str = $self->string_peek(@_) // return undef;
209 323         757 my $cleanup = erase_scoped $str;
210 323         3617 return $self->_expand_string($str);
211             }
212              
213              
214             sub other_strings {
215 5     5 1 8 my $self = shift;
216 5   50     14 my $delim = shift // "\n";
217              
218 5         7 my @strings = map { $self->string_value($_) } grep { !$STANDARD_STRINGS{$_} } sort keys %{$self->strings};
  3         9  
  28         48  
  5         12  
219 5         18 return join($delim, @strings);
220             }
221              
222              
223             sub string_peek {
224 325     325 1 417 my $self = shift;
225 325         608 my $string = $self->string(@_);
226 325 100       958 return defined $string->{value} ? $string->{value} : $self->kdbx->peek($string);
227             }
228              
229             ##############################################################################
230              
231              
232             sub add_auto_type_association {
233 2     2 1 12 my $self = shift;
234 2         3 my $association = shift;
235 2         2 push @{$self->auto_type_associations}, $association;
  2         4  
236             }
237              
238              
239             sub expand_keystroke_sequence {
240 0     0 1 0 my $self = shift;
241 0         0 my $association = shift;
242              
243 0         0 my $keys;
244 0 0       0 if ($association) {
245             $keys = is_hashref($association) && exists $association->{keystroke_sequence} ?
246 0 0 0     0 $association->{keystroke_sequence} : defined $association ? $association : '';
    0          
247             }
248              
249 0 0       0 $keys = $self->auto_type_default_sequence if !$keys;
250             # TODO - Fall back to getting default sequence from parent group, which probably means we shouldn't be
251             # setting a default value in the entry..
252              
253 0         0 return $self->_expand_string($keys);
254             }
255              
256             ##############################################################################
257              
258              
259             sub binary {
260 3     3 1 7 my $self = shift;
261 3 50       21 my %args = @_ == 2 ? (key => shift, value => shift)
    50          
262             : @_ % 2 == 1 ? (key => shift, @_) : @_;
263              
264 3 0 33     13 if (!defined $args{key} && !defined $args{value}) {
265 0         0 my %standard = (value => 1, protect => 1);
266 0         0 my @other_keys = grep { !$standard{$_} } keys %args;
  0         0  
267 0 0       0 if (@other_keys == 1) {
268 0         0 my $key = $args{key} = $other_keys[0];
269 0         0 $args{value} = delete $args{$key};
270             }
271             }
272              
273 3 50       9 my $key = delete $args{key} or throw 'Must provide a binary key to access';
274              
275 3 50       12 return $self->{binaries}{$key} = $args{value} if is_plain_hashref($args{value});
276              
277 3   0 0   24 assert { !defined $args{value} || !utf8::is_utf8($args{value}) };
  0         0  
278 3         19 while (my ($field, $value) = each %args) {
279 0         0 $self->{binaries}{$key}{$field} = $value;
280             }
281 3         17 return $self->{binaries}{$key};
282             }
283              
284              
285             sub binary_value {
286 1     1 1 3 my $self = shift;
287 1   50     5 my $binary = $self->binary(@_) // return undef;
288 1         5 return $binary->{value};
289             }
290              
291             ##############################################################################
292              
293              
294             sub hmac_otp {
295 27     27 1 44 my $self = shift;
296 27         82 load_optional('Pass::OTP');
297              
298 27         72 my %params = ($self->_hotp_params, @_);
299 27 50 33     133 return if !defined $params{type} || !defined $params{secret};
300              
301 27 100       91 $params{secret} = encode_b32r($params{secret}) if !$params{base32};
302 27         49 $params{base32} = 1;
303              
304 27         34 my $otp = eval { Pass::OTP::otp(%params, @_) };
  27         116  
305 27 50       13377 if (my $err = $@) {
306 0         0 throw 'Unable to generate HOTP', error => $err;
307             }
308              
309 27         106 $self->_hotp_increment_counter($params{counter});
310              
311 27         141 return $otp;
312             }
313              
314              
315             sub time_otp {
316 10     10 1 21 my $self = shift;
317 10         39 load_optional('Pass::OTP');
318              
319 10         27 my %params = ($self->_totp_params, @_);
320 10 50 33     59 return if !defined $params{type} || !defined $params{secret};
321              
322 10 50       24 $params{secret} = encode_b32r($params{secret}) if !$params{base32};
323 10         16 $params{base32} = 1;
324              
325 10         15 my $otp = eval { Pass::OTP::otp(%params, @_) };
  10         62  
326 10 50       7391 if (my $err = $@) {
327 0         0 throw 'Unable to generate TOTP', error => $err;
328             }
329              
330 10         63 return $otp;
331             }
332              
333              
334 9     9 1 66 sub hmac_otp_uri { $_[0]->_otp_uri($_[0]->_hotp_params) }
335 11     11 1 105 sub time_otp_uri { $_[0]->_otp_uri($_[0]->_totp_params) }
336              
337             sub _otp_uri {
338 20     20   42 my $self = shift;
339 20         69 my %params = @_;
340              
341 20 50       66 return if 4 != grep { defined } @params{qw(type secret issuer account)};
  80         150  
342 20 50       110 return if $params{type} !~ /^[ht]otp$/i;
343              
344 20         39 my $label = delete $params{label};
345 20         108 $params{$_} = uri_escape_utf8($params{$_}) for keys %params;
346              
347 20         46 my $type = lc($params{type});
348 20         36 my $issuer = $params{issuer};
349 20         78 my $account = $params{account};
350              
351 20   66     79 $label //= "$issuer:$account";
352              
353 20         39 my $secret = $params{secret};
354 20 100       67 $secret = uc(encode_b32r($secret)) if !$params{base32};
355              
356 20 100 100     81 delete $params{algorithm} if defined $params{algorithm} && $params{algorithm} eq 'sha1';
357 20 100 100     98 delete $params{period} if defined $params{period} && $params{period} == 30;
358 20 100 66     84 delete $params{digits} if defined $params{digits} && $params{digits} == 6;
359 20 100 100     67 delete $params{counter} if defined $params{counter} && $params{counter} == 0;
360              
361 20         65 my $uri = "otpauth://$type/$label?secret=$secret&issuer=$issuer";
362              
363 20 100       44 if (defined $params{encoder}) {
364 1         3 $uri .= "&encoder=$params{encoder}";
365 1         9 return $uri;
366             }
367 19 100       51 $uri .= '&algorithm=' . uc($params{algorithm}) if defined $params{algorithm};
368 19 100       46 $uri .= "&digits=$params{digits}" if defined $params{digits};
369 19 100       52 $uri .= "&counter=$params{counter}" if defined $params{counter};
370 19 100       40 $uri .= "&period=$params{period}" if defined $params{period};
371              
372 19         151 return $uri;
373             }
374              
375             sub _hotp_params {
376 36     36   58 my $self = shift;
377              
378 36   100     84 my %params = (
      100        
      100        
379             type => 'hotp',
380             issuer => $self->expand_title || 'KDBX',
381             account => $self->expand_username || 'none',
382             digits => 6,
383             counter => $self->string_value('HmacOtp-Counter') // 0,
384             $self->_otp_secret_params('Hmac'),
385             );
386 36 100       752 return %params if $params{secret};
387              
388 12         30 my %otp_params = $self->_otp_params;
389 12 50 33     56 return () if !$otp_params{secret} || $otp_params{type} ne 'hotp';
390              
391             # $otp_params{counter} = 0
392              
393 12         75 return (%params, %otp_params);
394             }
395              
396             sub _totp_params {
397 21     21   34 my $self = shift;
398              
399 21         79 my %algorithms = (
400             'HMAC-SHA-1' => 'sha1',
401             'HMAC-SHA-256' => 'sha256',
402             'HMAC-SHA-512' => 'sha512',
403             );
404             my %params = (
405             type => 'totp',
406             issuer => $self->expand_title || 'KDBX',
407             account => $self->expand_username || 'none',
408             digits => $self->string_value('TimeOtp-Length') // 6,
409 21   100     70 algorithm => $algorithms{$self->string_value('TimeOtp-Algorithm') || ''} || 'sha1',
      100        
      100        
      100        
      100        
410             period => $self->string_value('TimeOtp-Period') // 30,
411             $self->_otp_secret_params('Time'),
412             );
413 21 100       104 return %params if $params{secret};
414              
415 10         27 my %otp_params = $self->_otp_params;
416 10 50 33     46 return () if !$otp_params{secret} || $otp_params{type} ne 'totp';
417              
418 10         107 return (%params, %otp_params);
419             }
420              
421             # KeePassXC style
422             sub _otp_params {
423 22     22   30 my $self = shift;
424 22         62 load_optional('Pass::OTP::URI');
425              
426 22   50     50 my $uri = $self->string_value('otp') || '';
427 22         32 my %params;
428 22 50       132 %params = Pass::OTP::URI::parse($uri) if $uri =~ m!^otpauth://!;
429 22 50 33     710 return () if !$params{secret} || !$params{type};
430              
431 22 100 100     131 if (($params{encoder} // '') eq 'steam') {
432 2         3 $params{digits} = 5;
433 2         6 $params{chars} = '23456789BCDFGHJKMNPQRTVWXY';
434             }
435              
436             # Pass::OTP::URI doesn't provide the issuer and account separately, so get them from the label
437 22   50     133 my ($issuer, $user) = split(':', $params{label} // ':', 2);
438 22   66     69 $params{issuer} //= uri_unescape_utf8($issuer);
439 22   33     208 $params{account} //= uri_unescape_utf8($user);
440              
441 22 100       1356 $params{algorithm} = lc($params{algorithm}) if $params{algorithm};
442 22 100       68 $params{counter} = $self->string_value('HmacOtp-Counter') if $params{type} eq 'hotp';
443              
444 22         127 return %params;
445             }
446              
447             sub _otp_secret_params {
448 57     57   114 my $self = shift;
449 57   50     100 my $type = shift // return ();
450              
451 57         140 my $secret_txt = $self->string_value("${type}Otp-Secret");
452 57         153 my $secret_hex = $self->string_value("${type}Otp-Secret-Hex");
453 57         132 my $secret_b32 = $self->string_value("${type}Otp-Secret-Base32");
454 57         132 my $secret_b64 = $self->string_value("${type}Otp-Secret-Base64");
455              
456 57         100 my $count = grep { defined } ($secret_txt, $secret_hex, $secret_b32, $secret_b64);
  228         330  
457 57 100       186 return () if $count == 0;
458 35 100       80 alert "Found multiple ${type}Otp-Secret strings", count => $count if 1 < $count;
459              
460 35 100       145 return (secret => $secret_b32, base32 => 1) if defined $secret_b32;
461 20 100       60 return (secret => decode_b64($secret_b64)) if defined $secret_b64;
462 16 100       55 return (secret => pack('H*', $secret_hex)) if defined $secret_hex;
463 12         46 return (secret => encode('UTF-8', $secret_txt));
464             }
465              
466             sub _hotp_increment_counter {
467 27     27   42 my $self = shift;
468 27   100     87 my $counter = shift // $self->string_value('HmacOtp-Counter') || 0;
469              
470 27 50       85 looks_like_number($counter) or throw 'HmacOtp-Counter value must be a number', value => $counter;
471 27         48 my $next = $counter + 1;
472 27         69 $self->string('HmacOtp-Counter', $next);
473 27         36 return $next;
474             }
475              
476             ##############################################################################
477              
478              
479             sub size {
480 3     3 1 5 my $self = shift;
481              
482 3         3 my $size = 0;
483              
484             # tags
485 3   50     7 $size += length(encode('UTF-8', $self->tags // ''));
486              
487             # attributes (strings)
488 3         264 while (my ($key, $string) = each %{$self->strings}) {
  18         908  
489 15 50       22 next if !defined $string->{value};
490 15   50     25 $size += length(encode('UTF-8', $key)) + length(encode('UTF-8', $string->{value} // ''));
491             }
492              
493             # custom data
494 3         5 while (my ($key, $item) = each %{$self->custom_data}) {
  3         7  
495 0 0       0 next if !defined $item->{value};
496 0   0     0 $size += length(encode('UTF-8', $key)) + length(encode('UTF-8', $item->{value} // ''));
497             }
498              
499             # binaries
500 3         6 while (my ($key, $binary) = each %{$self->binaries}) {
  3         6  
501 0 0       0 next if !defined $binary->{value};
502             my $value_len = utf8::is_utf8($binary->{value}) ? length(encode('UTF-8', $binary->{value}))
503 0 0       0 : length($binary->{value});
504 0         0 $size += length(encode('UTF-8', $key)) + $value_len;
505             }
506              
507             # autotype associations
508 3 50       6 for my $association (@{$self->auto_type->{associations} || []}) {
  3         7  
509             $size += length(encode('UTF-8', $association->{window}))
510 0   0     0 + length(encode('UTF-8', $association->{keystroke_sequence} // ''));
511             }
512              
513 3         13 return $size;
514             }
515              
516             ##############################################################################
517              
518             sub history {
519 280     280 1 408 my $self = shift;
520 280   100     731 my $entries = $self->{history} //= [];
521 280 100 100     714 if (@$entries && !blessed($entries->[0])) {
522 4         11 @$entries = map { $self->_wrap_entry($_, $self->kdbx) } @$entries;
  6         15  
523             }
524 280     0   1153 assert { !any { !blessed $_ } @$entries };
  0         0  
  0         0  
525 280         1051 return $entries;
526             }
527              
528              
529             sub history_size {
530 2     2 1 3 my $self = shift;
531 2         3 return sum0 map { $_->size } @{$self->history};
  3         8  
  2         4  
532             }
533              
534              
535             sub prune_history {
536 2     2 1 4 my $self = shift;
537 2         6 my %args = @_;
538              
539 2   33     8 my $max_items = $args{max_items} // eval { $self->kdbx->history_max_items } // HISTORY_DEFAULT_MAX_ITEMS;
  0   50     0  
540 2   33     5 my $max_size = $args{max_size} // eval { $self->kdbx->history_max_size } // HISTORY_DEFAULT_MAX_SIZE;
  0   50     0  
541 2   33     7 my $max_age = $args{max_age} // eval { $self->kdbx->maintenance_history_days } // HISTORY_DEFAULT_MAX_AGE;
  0   50     0  
542              
543             # history is ordered oldest to newest
544 2         65 my $history = $self->history;
545              
546 2         4 my @removed;
547              
548 2 50 33     8 if (0 <= $max_items && $max_items < @$history) {
549 0         0 push @removed, splice @$history, -$max_items;
550             }
551              
552 2 50       7 if (0 <= $max_size) {
553 2         6 my $current_size = $self->history_size;
554 2         5 while ($max_size < $current_size) {
555 0         0 push @removed, my $entry = shift @$history;
556 0         0 $current_size -= $entry->size;
557             }
558             }
559              
560 2 50       6 if (0 <= $max_age) {
561 2         8 my $cutoff = gmtime - ($max_age * 86400);
562 2         110 for (my $i = @$history - 1; 0 <= $i; --$i) {
563 3         36 my $entry = $history->[$i];
564 3 100       7 next if $cutoff <= $entry->last_modification_time;
565 1         30 push @removed, splice @$history, $i, 1;
566             }
567             }
568              
569 2         49 @removed = sort { $a->last_modification_time <=> $b->last_modification_time } @removed;
  0         0  
570 2         10 return @removed;
571             }
572              
573              
574             sub add_historical_entry {
575 8     8 1 15 my $self = shift;
576 8         43 delete $_->{history} for @_;
577 8   100     14 push @{$self->{history} //= []}, map { $self->_wrap_entry($_) } @_;
  8         30  
  8         29  
578             }
579              
580              
581             sub remove_historical_entry {
582 33     33 1 50 my $self = shift;
583 33         42 my $entry = shift;
584 33         64 my $history = $self->history;
585              
586 33         48 my @removed;
587 33         121 for (my $i = @$history - 1; 0 <= $i; --$i) {
588 3         4 my $item = $history->[$i];
589 3 50       12 next if Hash::Util::FieldHash::id($entry) != Hash::Util::FieldHash::id($item);
590 0         0 push @removed, splice @{$self->{history}}, $i, 1;
  0         0  
591             }
592 33         80 return @removed;
593             }
594              
595              
596             sub current_entry {
597 50     50 1 69 my $self = shift;
598 50         117 my $parent = $self->group;
599              
600 50 100       103 if ($parent) {
601 9         20 my $id = $self->uuid;
602 9     9   22 my $entry = first { $id eq $_->uuid } @{$parent->entries};
  9         16  
  9         20  
603 9 50       31 return $entry if $entry;
604             }
605              
606 41         69 return $self;
607             }
608              
609              
610             sub is_current {
611 16     16 1 28 my $self = shift;
612 16         34 my $current = $self->current_entry;
613 16         93 return Hash::Util::FieldHash::id($self) == Hash::Util::FieldHash::id($current);
614             }
615              
616              
617 1     1 1 3 sub is_historical { !$_[0]->is_current }
618              
619              
620             sub remove {
621 33     33 1 46 my $self = shift;
622 33         77 my $current = $self->current_entry;
623 33 50       78 return $self if $current->remove_historical_entry($self);
624 33         133 $self->SUPER::remove(@_);
625             }
626              
627             ##############################################################################
628              
629              
630             sub searching_enabled {
631 0     0 1 0 my $self = shift;
632 0         0 my $parent = $self->group;
633 0 0       0 return $parent->effective_enable_searching if $parent;
634 0         0 return true;
635             }
636              
637             sub auto_type_enabled {
638 84     84 1 148 my $self = shift;
639 84 50       167 $self->auto_type->{enabled} = to_bool(shift) if @_;
640 84   33     189 $self->auto_type->{enabled} //= true;
641 84 50       803 return false if !$self->auto_type->{enabled};
642 84 100       690 return true if !$self->is_connected;
643 25         81 my $parent = $self->group;
644 25 50       75 return $parent->effective_enable_auto_type if $parent;
645 25         58 return true;
646             }
647              
648             ##############################################################################
649              
650             sub _signal {
651 45     45   62 my $self = shift;
652 45         62 my $type = shift;
653 45         169 return $self->SUPER::_signal("entry.$type", @_);
654             }
655              
656             sub _commit {
657 8     8   12 my $self = shift;
658 8         9 my $orig = shift;
659 8         26 $self->add_historical_entry($orig);
660 8         31 my $time = gmtime;
661 8         500 $self->last_modification_time($time);
662 8         102 $self->last_access_time($time);
663             }
664              
665 138     138 1 351 sub label { shift->expand_title(@_) }
666              
667             ### Name of the parent attribute expected to contain the object
668 22     22   37 sub _parent_container { 'entries' }
669              
670             1;
671              
672             __END__