File Coverage

blib/lib/File/KDBX/Entry.pm
Criterion Covered Total %
statement 417 448 93.0
branch 165 252 65.4
condition 123 198 62.1
subroutine 82 86 95.3
pod 48 50 96.0
total 835 1034 80.7


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