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   367245 use warnings;
  10         22  
  10         345  
5 10     10   48 use strict;
  10         21  
  10         362  
6              
7 10     10   2772 use Crypt::Misc 0.049 qw(decode_b64 encode_b32r);
  10         41714  
  10         664  
8 10     10   1976 use Devel::GlobalDestruction;
  10         2462  
  10         72  
9 10     10   575 use Encode qw(encode);
  10         17  
  10         359  
10 10     10   99 use File::KDBX::Constants qw(:history :icon);
  10         17  
  10         4272  
11 10     10   66 use File::KDBX::Error;
  10         19  
  10         549  
12 10     10   59 use File::KDBX::Util qw(:assert :class :coercion :erase :function :uri generate_uuid load_optional);
  10         22  
  10         1908  
13 10     10   2444 use Hash::Util::FieldHash;
  10         4089  
  10         399  
14 10     10   64 use List::Util qw(any first sum0);
  10         20  
  10         564  
15 10     10   1322 use Ref::Util qw(is_coderef is_hashref is_plain_hashref);
  10         19  
  10         439  
16 10     10   52 use Scalar::Util qw(blessed looks_like_number);
  10         15  
  10         405  
17 10     10   6450 use Storable qw(dclone);
  10         29903  
  10         628  
18 10     10   68 use Time::Piece 1.33;
  10         203  
  10         66  
19 10     10   730 use boolean;
  10         17  
  10         70  
20 10     10   506 use namespace::clean;
  10         19  
  10         73  
21              
22             extends 'File::KDBX::Object';
23              
24             our $VERSION = '0.905'; # 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     1411 if (@_ || !defined $self->{uuid}) {
34 98 100       294 my %args = @_ % 2 == 1 ? (uuid => shift, @_) : @_;
35 98         173 my $old_uuid = $self->{uuid};
36 98   66     382 my $uuid = $self->{uuid} = delete $args{uuid} // generate_uuid;
37 98         215 for my $entry (@{$self->history}) {
  98         230  
38 6         15 $entry->{uuid} = $uuid;
39             }
40 98 100 66     311 $self->_signal('uuid.changed', $uuid, $old_uuid) if defined $old_uuid && $self->is_current;
41             }
42 449         1072 $self->{uuid};
43 182 50   182 1 635 }
44 182 100   98 1 707  
  98 50       302  
45 182 100 66 84 1 1340 # has uuid => sub { generate_uuid(printable => 1) };
  98 50       258  
  84         254  
46 98 50 100 84 1 477 has icon_id => ICON_PASSWORD, coerce => \&to_icon_constant;
  84 50       184  
  84         249  
47 84 50 33 84 1 589 has custom_icon_uuid => undef, coerce => \&to_uuid;
  84 50       200  
  84         272  
48 84 50 33 87 1 429 has foreground_color => '', coerce => \&to_string;
  84 50       187  
  87         250  
49 84 50 33 511 0 404 has background_color => '', coerce => \&to_string;
  87 50       177  
  511         1168  
50 87 50 66 113 1 456 has override_url => '', coerce => \&to_string;
  511 50       858  
  113         345  
51 511 100 100 138 1 2136 has tags => '', coerce => \&to_string;
  113 50       262  
  138         478  
52 113 100 100 134 1 566 has auto_type => {};
  138 50       277  
  134         777  
53 138 50 66 134 1 566 has previous_parent_group => undef, coerce => \&to_uuid;
  134 50       292  
  134         1555  
54 134 50 50 720 0 499 has quality_check => true, coerce => \&to_bool;
  134 50       305  
  720         1767  
55 134 50 100     729 has strings => {};
  720         1115  
56 720   100     3518 has binaries => {};
57             has times => {};
58 100 50   100 1 357 # has custom_data => {};
59 100 100   87 1 245 # has history => [];
  87 50       8813  
60 100 100 100 92 1 210  
  87 50       254  
  92         5037  
61 87 100 100 84 1 200 has last_modification_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  92 50       230  
  84         4815  
62 92 50 100 84 1 223 has creation_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  84 50       213  
  84         4752  
63 84 50 50 84 1 190 has last_access_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  84 50       237  
  84         638  
64 84 50 33 85 1 214 has expiry_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  84 50       197  
  85         242  
65 84 100 33     159 has expires => false, store => 'times', coerce => \&to_bool;
  85         212  
66 85   100     165 has usage_count => 0, store => 'times', coerce => \&to_number;
67 84 50   84 1 4832 has location_changed => sub { gmtime }, store => 'times', coerce => \&to_time;
68 84 50       217  
69 84 50 33 84 1 205 # has 'auto_type.auto_type_enabled' => true, coerce => \&to_bool;
  84         272  
70 84 50       214 has 'auto_type_obfuscation' => 0, path => 'auto_type.data_transfer_obfuscation',
71 84 50 33 87 1 165 coerce => \&to_number;
  87         276  
72 87 50       210 has 'auto_type_default_sequence' => '{USERNAME}{TAB}{PASSWORD}{ENTER}',
73 87   100     185 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   16058 no strict 'refs'; ## no critic (ProhibitNoStrict)
  10         20  
  10         42874  
85 479     479   6193 *{$attr} = sub { shift->string_value($string_key, @_) };
86 323     323   1482 *{"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   127 my $self = shift;
92 84         414 $self->$_ for @ATTRS, keys %ATTRS_STRINGS, list_attributes(ref $self);
93             }
94              
95             sub init {
96 104     104 1 174 my $self = shift;
97 104         251 my %args = @_;
98              
99 104         376 while (my ($key, $val) = each %args) {
100 156 100       623 if (my $method = $self->can($key)) {
101 154         364 $self->$method($val);
102             }
103             else {
104 2         4 $self->string($key => $val);
105             }
106             }
107              
108 104         224 return $self;
109             }
110              
111             ##############################################################################
112              
113              
114             sub string {
115 1263     1263 1 1603 my $self = shift;
116 1263 50       3746 my %args = @_ == 2 ? (key => shift, value => shift)
    100          
117             : @_ % 2 == 1 ? (key => shift, @_) : @_;
118              
119 1263 0 33     2248 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       2350 my $key = delete $args{key} or throw 'Must provide a string key to access';
129              
130 1263 50       3477 return $self->{strings}{$key} = $args{value} if is_plain_hashref($args{value});
131              
132 1263         3218 while (my ($field, $value) = each %args) {
133 145         561 $self->{strings}{$key}{$field} = $value;
134             }
135              
136             # Auto-vivify the standard strings.
137 1263 100       2345 if ($STANDARD_STRINGS{$key}) {
138 809 100 100     2609 return $self->{strings}{$key} //= {value => '', $self->_protect($key) ? (protect => true) : ()};
139             }
140 454         1137 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   464 my $self = shift;
146 348         404 my $key = shift;
147 348 50       609 return false if !$STANDARD_STRINGS{$key};
148 348 100       449 if (my $kdbx = eval { $self->kdbx }) {
  348         1333  
149 89         172 my $protect = $kdbx->memory_protection($key);
150 89 50       294 return $protect if defined $protect;
151             }
152 259         1514 return $key eq 'Password';
153             }
154              
155              
156             sub string_value {
157 875     875 1 12638 my $self = shift;
158 875   100     1371 my $string = $self->string(@_) // return undef;
159 599         3129 return $string->{value};
160             }
161              
162              
163             sub _expand_placeholder {
164 43     43   70 my $self = shift;
165 43         56 my $placeholder = shift;
166 43         61 my $arg = shift;
167              
168 43         157 require File::KDBX;
169              
170 43         68 my $placeholder_key = $placeholder;
171 43 100       77 if (defined $arg) {
172 12 50       48 $placeholder_key = $File::KDBX::PLACEHOLDERS{"${placeholder}:${arg}"} ? "${placeholder}:${arg}"
173             : "${placeholder}:";
174             }
175 43 100       101 return if !defined $File::KDBX::PLACEHOLDERS{$placeholder_key};
176              
177 42         112 my $local_key = join('/', Hash::Util::FieldHash::id($self), $placeholder_key);
178 42   66     100 local $PLACEHOLDERS{$local_key} = my $handler = $PLACEHOLDERS{$local_key} // do {
179 23 50       50 my $handler = $File::KDBX::PLACEHOLDERS{$placeholder_key} or next;
180             memoize recurse_limit($handler, $PLACEHOLDER_MAX_DEPTH, sub {
181 1     1   8 alert "Detected deep recursion while expanding $placeholder placeholder",
182             placeholder => $placeholder;
183 1         30 return; # undef
184 23         103 });
185             };
186              
187 42         126 return $handler->($self, $arg, $placeholder);
188             }
189              
190             sub _expand_string {
191 323     323   403 my $self = shift;
192 323         430 my $str = shift;
193              
194 323         1176 my $expand = memoize $self->can('_expand_placeholder'), $self;
195              
196             # placeholders (including field references):
197 323   66     838 $str =~ s!\{([^:\}]+)(?::([^\}]*))?\}!$expand->(uc($1), $2, @_) // $&!egi;
  44         197  
198              
199             # environment variables (alt syntax):
200 323         1814 my $vars = join('|', map { quotemeta($_) } keys %ENV);
  10984         14995  
201 323   33     3443 $str =~ s!\%($vars)\%!$expand->(ENV => $1, @_) // $&!eg;
  2         11  
202              
203 323         1927 return $str;
204             }
205              
206             sub expand_string_value {
207 323     323 1 406 my $self = shift;
208 323   50     596 my $str = $self->string_peek(@_) // return undef;
209 323         809 my $cleanup = erase_scoped $str;
210 323         3581 return $self->_expand_string($str);
211             }
212              
213              
214             sub other_strings {
215 5     5 1 9 my $self = shift;
216 5   50     16 my $delim = shift // "\n";
217              
218 5         7 my @strings = map { $self->string_value($_) } grep { !$STANDARD_STRINGS{$_} } sort keys %{$self->strings};
  3         8  
  28         48  
  5         12  
219 5         18 return join($delim, @strings);
220             }
221              
222              
223             sub string_peek {
224 325     325 1 424 my $self = shift;
225 325         622 my $string = $self->string(@_);
226 325 100       983 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 10 my $self = shift;
234 2         3 my $association = shift;
235 2         1 push @{$self->auto_type_associations}, $association;
  2         5  
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       25 my %args = @_ == 2 ? (key => shift, value => shift)
    50          
262             : @_ % 2 == 1 ? (key => shift, @_) : @_;
263              
264 3 0 33     11 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       12 my $key = delete $args{key} or throw 'Must provide a binary key to access';
274              
275 3 50       16 return $self->{binaries}{$key} = $args{value} if is_plain_hashref($args{value});
276              
277 3   0 0   20 assert { !defined $args{value} || !utf8::is_utf8($args{value}) };
  0         0  
278 3         18 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     2 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 35 my $self = shift;
296 27         73 load_optional('Pass::OTP');
297              
298 27         53 my %params = ($self->_hotp_params, @_);
299 27 50 33     92 return if !defined $params{type} || !defined $params{secret};
300              
301 27 100       69 $params{secret} = encode_b32r($params{secret}) if !$params{base32};
302 27         36 $params{base32} = 1;
303              
304 27         40 my $otp = eval { Pass::OTP::otp(%params, @_) };
  27         86  
305 27 50       11024 if (my $err = $@) {
306 0         0 throw 'Unable to generate HOTP', error => $err;
307             }
308              
309 27         73 $self->_hotp_increment_counter($params{counter});
310              
311 27         120 return $otp;
312             }
313              
314              
315             sub time_otp {
316 10     10 1 11 my $self = shift;
317 10         31 load_optional('Pass::OTP');
318              
319 10         20 my %params = ($self->_totp_params, @_);
320 10 50 33     44 return if !defined $params{type} || !defined $params{secret};
321              
322 10 50       15 $params{secret} = encode_b32r($params{secret}) if !$params{base32};
323 10         17 $params{base32} = 1;
324              
325 10         13 my $otp = eval { Pass::OTP::otp(%params, @_) };
  10         35  
326 10 50       5782 if (my $err = $@) {
327 0         0 throw 'Unable to generate TOTP', error => $err;
328             }
329              
330 10         52 return $otp;
331             }
332              
333              
334 9     9 1 38 sub hmac_otp_uri { $_[0]->_otp_uri($_[0]->_hotp_params) }
335 11     11 1 71 sub time_otp_uri { $_[0]->_otp_uri($_[0]->_totp_params) }
336              
337             sub _otp_uri {
338 20     20   27 my $self = shift;
339 20         63 my %params = @_;
340              
341 20 50       37 return if 4 != grep { defined } @params{qw(type secret issuer account)};
  80         128  
342 20 50       65 return if $params{type} !~ /^[ht]otp$/i;
343              
344 20         29 my $label = delete $params{label};
345 20         77 $params{$_} = uri_escape_utf8($params{$_}) for keys %params;
346              
347 20         37 my $type = lc($params{type});
348 20         26 my $issuer = $params{issuer};
349 20         29 my $account = $params{account};
350              
351 20   66     66 $label //= "$issuer:$account";
352              
353 20         25 my $secret = $params{secret};
354 20 100       44 $secret = uc(encode_b32r($secret)) if !$params{base32};
355              
356 20 100 100     63 delete $params{algorithm} if defined $params{algorithm} && $params{algorithm} eq 'sha1';
357 20 100 100     68 delete $params{period} if defined $params{period} && $params{period} == 30;
358 20 100 66     68 delete $params{digits} if defined $params{digits} && $params{digits} == 6;
359 20 100 100     46 delete $params{counter} if defined $params{counter} && $params{counter} == 0;
360              
361 20         48 my $uri = "otpauth://$type/$label?secret=$secret&issuer=$issuer";
362              
363 20 100       34 if (defined $params{encoder}) {
364 1         7 $uri .= "&encoder=$params{encoder}";
365 1         7 return $uri;
366             }
367 19 100       29 $uri .= '&algorithm=' . uc($params{algorithm}) if defined $params{algorithm};
368 19 100       38 $uri .= "&digits=$params{digits}" if defined $params{digits};
369 19 100       28 $uri .= "&counter=$params{counter}" if defined $params{counter};
370 19 100       29 $uri .= "&period=$params{period}" if defined $params{period};
371              
372 19         138 return $uri;
373             }
374              
375             sub _hotp_params {
376 36     36   42 my $self = shift;
377              
378 36   100     66 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       644 return %params if $params{secret};
387              
388 12         20 my %otp_params = $self->_otp_params;
389 12 50 33     42 return () if !$otp_params{secret} || $otp_params{type} ne 'hotp';
390              
391             # $otp_params{counter} = 0
392              
393 12         73 return (%params, %otp_params);
394             }
395              
396             sub _totp_params {
397 21     21   71 my $self = shift;
398              
399 21         74 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     37 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       91 return %params if $params{secret};
414              
415 10         19 my %otp_params = $self->_otp_params;
416 10 50 33     38 return () if !$otp_params{secret} || $otp_params{type} ne 'totp';
417              
418 10         71 return (%params, %otp_params);
419             }
420              
421             # KeePassXC style
422             sub _otp_params {
423 22     22   25 my $self = shift;
424 22         52 load_optional('Pass::OTP::URI');
425              
426 22   50     39 my $uri = $self->string_value('otp') || '';
427 22         27 my %params;
428 22 50       90 %params = Pass::OTP::URI::parse($uri) if $uri =~ m!^otpauth://!;
429 22 50 33     613 return () if !$params{secret} || !$params{type};
430              
431 22 100 100     66 if (($params{encoder} // '') eq 'steam') {
432 2         6 $params{digits} = 5;
433 2         4 $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     58 my ($issuer, $user) = split(':', $params{label} // ':', 2);
438 22   66     48 $params{issuer} //= uri_unescape_utf8($issuer);
439 22   33     150 $params{account} //= uri_unescape_utf8($user);
440              
441 22 100       1096 $params{algorithm} = lc($params{algorithm}) if $params{algorithm};
442 22 100       45 $params{counter} = $self->string_value('HmacOtp-Counter') if $params{type} eq 'hotp';
443              
444 22         106 return %params;
445             }
446              
447             sub _otp_secret_params {
448 57     57   76 my $self = shift;
449 57   50     84 my $type = shift // return ();
450              
451 57         119 my $secret_txt = $self->string_value("${type}Otp-Secret");
452 57         118 my $secret_hex = $self->string_value("${type}Otp-Secret-Hex");
453 57         113 my $secret_b32 = $self->string_value("${type}Otp-Secret-Base32");
454 57         126 my $secret_b64 = $self->string_value("${type}Otp-Secret-Base64");
455              
456 57         82 my $count = grep { defined } ($secret_txt, $secret_hex, $secret_b32, $secret_b64);
  228         339  
457 57 100       150 return () if $count == 0;
458 35 100       73 alert "Found multiple ${type}Otp-Secret strings", count => $count if 1 < $count;
459              
460 35 100       115 return (secret => $secret_b32, base32 => 1) if defined $secret_b32;
461 20 100       51 return (secret => decode_b64($secret_b64)) if defined $secret_b64;
462 16 100       42 return (secret => pack('H*', $secret_hex)) if defined $secret_hex;
463 12         32 return (secret => encode('UTF-8', $secret_txt));
464             }
465              
466             sub _hotp_increment_counter {
467 27     27   36 my $self = shift;
468 27   100     69 my $counter = shift // $self->string_value('HmacOtp-Counter') || 0;
469              
470 27 50       94 looks_like_number($counter) or throw 'HmacOtp-Counter value must be a number', value => $counter;
471 27         40 my $next = $counter + 1;
472 27         57 $self->string('HmacOtp-Counter', $next);
473 27         29 return $next;
474             }
475              
476             ##############################################################################
477              
478              
479             sub size {
480 3     3 1 4 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         245 while (my ($key, $string) = each %{$self->strings}) {
  18         898  
489 15 50       25 next if !defined $string->{value};
490 15   50     24 $size += length(encode('UTF-8', $key)) + length(encode('UTF-8', $string->{value} // ''));
491             }
492              
493             # custom data
494 3         3 while (my ($key, $item) = each %{$self->custom_data}) {
  3         8  
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         4 while (my ($key, $binary) = each %{$self->binaries}) {
  3         5  
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       4 for my $association (@{$self->auto_type->{associations} || []}) {
  3         6  
509             $size += length(encode('UTF-8', $association->{window}))
510 0   0     0 + length(encode('UTF-8', $association->{keystroke_sequence} // ''));
511             }
512              
513 3         11 return $size;
514             }
515              
516             ##############################################################################
517              
518             sub history {
519 280     280 1 397 my $self = shift;
520 280   100     730 my $entries = $self->{history} //= [];
521 280 100 100     688 if (@$entries && !blessed($entries->[0])) {
522 4         11 @$entries = map { $self->_wrap_entry($_, $self->kdbx) } @$entries;
  6         18  
523             }
524 280     0   1177 assert { !any { !blessed $_ } @$entries };
  0         0  
  0         0  
525 280         1052 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         6  
  2         4  
532             }
533              
534              
535             sub prune_history {
536 2     2 1 3 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     6 my $max_size = $args{max_size} // eval { $self->kdbx->history_max_size } // HISTORY_DEFAULT_MAX_SIZE;
  0   50     0  
541 2   33     5 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         57 my $history = $self->history;
545              
546 2         3 my @removed;
547              
548 2 50 33     9 if (0 <= $max_items && $max_items < @$history) {
549 0         0 push @removed, splice @$history, -$max_items;
550             }
551              
552 2 50       5 if (0 <= $max_size) {
553 2         4 my $current_size = $self->history_size;
554 2         13 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         107 for (my $i = @$history - 1; 0 <= $i; --$i) {
563 3         36 my $entry = $history->[$i];
564 3 100       33 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         9 return @removed;
571             }
572              
573              
574             sub add_historical_entry {
575 8     8 1 10 my $self = shift;
576 8         42 delete $_->{history} for @_;
577 8   100     11 push @{$self->{history} //= []}, map { $self->_wrap_entry($_) } @_;
  8         26  
  8         25  
578             }
579              
580              
581             sub remove_historical_entry {
582 33     33 1 46 my $self = shift;
583 33         42 my $entry = shift;
584 33         68 my $history = $self->history;
585              
586 33         49 my @removed;
587 33         91 for (my $i = @$history - 1; 0 <= $i; --$i) {
588 3         4 my $item = $history->[$i];
589 3 50       13 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         77 return @removed;
593             }
594              
595              
596             sub current_entry {
597 50     50 1 64 my $self = shift;
598 50         118 my $parent = $self->group;
599              
600 50 100       98 if ($parent) {
601 9         27 my $id = $self->uuid;
602 9     9   22 my $entry = first { $id eq $_->uuid } @{$parent->entries};
  9         16  
  9         25  
603 9 50       29 return $entry if $entry;
604             }
605              
606 41         67 return $self;
607             }
608              
609              
610             sub is_current {
611 16     16 1 22 my $self = shift;
612 16         29 my $current = $self->current_entry;
613 16         85 return Hash::Util::FieldHash::id($self) == Hash::Util::FieldHash::id($current);
614             }
615              
616              
617 1     1 1 5 sub is_historical { !$_[0]->is_current }
618              
619              
620             sub remove {
621 33     33 1 46 my $self = shift;
622 33         84 my $current = $self->current_entry;
623 33 50       93 return $self if $current->remove_historical_entry($self);
624 33         104 $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 157 my $self = shift;
639 84 50       166 $self->auto_type->{enabled} = to_bool(shift) if @_;
640 84   33     164 $self->auto_type->{enabled} //= true;
641 84 50       863 return false if !$self->auto_type->{enabled};
642 84 100       681 return true if !$self->is_connected;
643 25         78 my $parent = $self->group;
644 25 50       67 return $parent->effective_enable_auto_type if $parent;
645 25         58 return true;
646             }
647              
648             ##############################################################################
649              
650             sub _signal {
651 45     45   61 my $self = shift;
652 45         62 my $type = shift;
653 45         175 return $self->SUPER::_signal("entry.$type", @_);
654             }
655              
656             sub _commit {
657 8     8   15 my $self = shift;
658 8         11 my $orig = shift;
659 8         22 $self->add_historical_entry($orig);
660 8         25 my $time = gmtime;
661 8         431 $self->last_modification_time($time);
662 8         97 $self->last_access_time($time);
663             }
664              
665 138     138 1 406 sub label { shift->expand_title(@_) }
666              
667             ### Name of the parent attribute expected to contain the object
668 22     22   36 sub _parent_container { 'entries' }
669              
670             1;
671              
672             __END__