File Coverage

blib/lib/File/KDBX.pm
Criterion Covered Total %
statement 565 648 87.1
branch 225 398 56.5
condition 158 308 51.3
subroutine 140 157 89.1
pod 101 104 97.1
total 1189 1615 73.6


line stmt bran cond sub pod time code
1             package File::KDBX;
2             # ABSTRACT: Encrypted database to store secret text and files
3              
4 12     12   630866 use 5.010;
  12         42  
5 12     12   58 use warnings;
  12         23  
  12         331  
6 12     12   55 use strict;
  12         236  
  12         314  
7              
8 12     12   3087 use Crypt::Digest qw(digest_data);
  12         5121  
  12         581  
9 12     12   75 use Crypt::PRNG qw(random_bytes);
  12         24  
  12         448  
10 12     12   2570 use Devel::GlobalDestruction;
  12         3265  
  12         68  
11 12     12   678 use File::KDBX::Constants qw(:all :icon);
  12         23  
  12         9835  
12 12     12   85 use File::KDBX::Error;
  12         17  
  12         543  
13 12     12   4345 use File::KDBX::Safe;
  12         37  
  12         409  
14 12     12   70 use File::KDBX::Util qw(:class :coercion :empty :search :uuid erase simple_expression_query snakify);
  12         18  
  12         2151  
15 12     12   4217 use Hash::Util::FieldHash qw(fieldhashes);
  12         5487  
  12         1484  
16 12     12   67 use List::Util qw(any first);
  12         19  
  12         1486  
17 12     12   58 use Ref::Util qw(is_ref is_arrayref is_plain_hashref);
  12         21  
  12         457  
18 12     12   1175 use Scalar::Util qw(blessed);
  12         1119  
  12         474  
19 12     12   62 use Time::Piece 1.33;
  12         225  
  12         97  
20 12     12   858 use boolean;
  12         52  
  12         1258  
21 12     12   612 use namespace::clean;
  12         23  
  12         49  
22              
23             our $VERSION = '0.906'; # VERSION
24             our $WARNINGS = 1;
25              
26             fieldhashes \my (%SAFE, %KEYS);
27              
28              
29             sub new {
30 62     62 1 88512 my $class = shift;
31              
32             # copy constructor
33 62 0 33     257 return $_[0]->clone if @_ == 1 && blessed $_[0] && $_[0]->isa($class);
      33        
34              
35 62         95 my $data;
36 62 50       261 $data = shift if is_plain_hashref($_[0]);
37              
38 62   50     341 my $self = bless $data // {}, $class;
39 62         222 $self->init(@_);
40 62 50       228 $self->_set_nonlazy_attributes if !$data;
41 62         371 return $self;
42             }
43              
44 59 50   59   25039 sub DESTROY { local ($., $@, $!, $^E, $?); !in_global_destruction and $_[0]->reset }
  59         1328  
45              
46              
47             sub init {
48 62     62 1 111 my $self = shift;
49 62         120 my %args = @_;
50              
51 62         167 @$self{keys %args} = values %args;
52              
53 62         110 return $self;
54             }
55              
56              
57             sub reset {
58 89     89 1 522 my $self = shift;
59 89         198 erase $self->headers->{+HEADER_INNER_RANDOM_STREAM_KEY};
60 89         216 erase $self->inner_headers->{+INNER_HEADER_INNER_RANDOM_STREAM_KEY};
61 89         256 erase $self->{raw};
62 89         2630 %$self = ();
63 89         456 $self->_remove_safe;
64 89         842 return $self;
65             }
66              
67              
68             sub clone {
69 1     1 1 1 my $self = shift;
70 1         4 require Storable;
71 1         30 return Storable::dclone($self);
72             }
73              
74             sub STORABLE_freeze {
75 1     1 0 4 my $self = shift;
76 1         2 my $cloning = shift;
77              
78 1         7 my $copy = {%$self};
79              
80 1   33     143 return '', $copy, $KEYS{$self} // (), $SAFE{$self} // ();
      33        
81             }
82              
83             sub STORABLE_thaw {
84 1     1 0 2 my $self = shift;
85 1         3 my $cloning = shift;
86 1         2 shift;
87 1         1 my $clone = shift;
88 1         2 my $key = shift;
89 1         2 my $safe = shift;
90              
91 1         6 @$self{keys %$clone} = values %$clone;
92 1         7 $KEYS{$self} = $key;
93 1         3 $SAFE{$self} = $safe;
94              
95             # Dualvars aren't cloned as dualvars, so coerce the compression flags.
96 1         3 $self->compression_flags($self->compression_flags);
97              
98 1     3   4 $self->objects(history => 1)->each(sub { $_->kdbx($self) });
  3         11  
99             }
100              
101             ##############################################################################
102              
103              
104 9     9 1 13452 sub load { shift->_loader->load(@_) }
105 21     21 1 1168 sub load_string { shift->_loader->load_string(@_) }
106 0     0 1 0 sub load_file { shift->_loader->load_file(@_) }
107 0     0 1 0 sub load_handle { shift->_loader->load_handle(@_) }
108              
109             sub _loader {
110 30     30   52 my $self = shift;
111 30 50       145 $self = $self->new if !ref $self;
112 30         2257 require File::KDBX::Loader;
113 30         176 File::KDBX::Loader->new(kdbx => $self);
114             }
115              
116              
117 2     2 1 2678 sub dump { shift->_dumper->dump(@_) }
118 13     13 1 210 sub dump_string { shift->_dumper->dump_string(@_) }
119 0     0 1 0 sub dump_file { shift->_dumper->dump_file(@_) }
120 0     0 1 0 sub dump_handle { shift->_dumper->dump_handle(@_) }
121              
122             sub _dumper {
123 15     15   30 my $self = shift;
124 15 50       54 $self = $self->new if !ref $self;
125 15         2847 require File::KDBX::Dumper;
126 15         105 File::KDBX::Dumper->new(kdbx => $self);
127             }
128              
129             ##############################################################################
130              
131              
132             sub user_agent_string {
133 15     15 1 76 require Config;
134             sprintf('%s/%s (%s/%s; %s/%s; %s)',
135 15 50   159 1 662 __PACKAGE__, $VERSION, @Config::Config{qw(package version osname osvers archname)});
  159         1089  
136 159 100   180 1 429 }
  180 50       498  
137 159 100 66 751 1 730  
  180 50       450  
  751         2204  
138 180 100 66 2153 1 650 has sig1 => KDBX_SIG1, coerce => \&to_number;
  751 50       1570  
  2153         31111  
139 751 50 66 424 1 3105 has sig2 => KDBX_SIG2_2, coerce => \&to_number;
  2153 50       3506  
  424         1045  
140 2153 50 100 1974 1 9031 has version => KDBX_VERSION_3_1, coerce => \&to_number;
  424 50       767  
  1974         28419  
141 424 50 100 109 1 1661 has headers => {};
  1974 50       3079  
  109         333  
142 1974 100 100 158 1 10025 has inner_headers => {};
  109 50       250  
  158         1119  
143 109 100 100 62 1 411 has meta => {};
  158 50       374  
  62         204  
144 158 50 100     738 has binaries => {};
  62         147  
145 62   50     322 has deleted_objects => {};
146 62 50   62 1 197 has raw => coerce => \&to_string;
147 62 50       145  
148 62 50 33 110 1 142 # HEADERS
  110         385  
149 110 50   64 1 289 has 'headers.comment' => '', coerce => \&to_string;
  64 50       257  
150 110 100 100 76 1 262 has 'headers.cipher_id' => sub { $_[0]->version < KDBX_VERSION_4_0 ? CIPHER_UUID_AES256 : CIPHER_UUID_CHACHA20 },
  64 50       162  
  76         324  
151 64 100 66     140 coerce => \&to_uuid;
  76         207  
152 76 50 100 124 1 162 has 'headers.compression_flags' => COMPRESSION_GZIP, coerce => \&to_compression_constant;
  124         2149  
153 124 100   76 1 324 has 'headers.master_seed' => sub { random_bytes(32) }, coerce => \&to_string;
  76 50       763  
154 124 100 100     251 has 'headers.encryption_iv' => sub { random_bytes($_[0]->version < KDBX_VERSION_4_0 ? 16 : 12) },
  76         214  
155 76   100     178 coerce => \&to_string;
156             has 'headers.stream_start_bytes' => sub { random_bytes(32) }, coerce => \&to_string;
157             has 'headers.kdf_parameters' => sub {
158             +{
159             KDF_PARAM_UUID() => KDF_UUID_AES,
160 350 50   350 1 4146 KDF_PARAM_AES_ROUNDS() => $_[0]->headers->{+HEADER_TRANSFORM_ROUNDS} // KDF_DEFAULT_AES_ROUNDS,
161 350 100       756 KDF_PARAM_AES_SEED() => $_[0]->headers->{+HEADER_TRANSFORM_SEED} // random_bytes(32),
162 350   100     678 };
163             };
164             # has 'headers.transform_seed' => sub { random_bytes(32) };
165             # has 'headers.transform_rounds' => 100_000;
166             # has 'headers.inner_random_stream_key' => sub { random_bytes(32) }; # 64 ?
167             # has 'headers.inner_random_stream_id' => STREAM_ID_CHACHA20;
168 62 50   62 1 793 # has 'headers.public_custom_data' => {};
169 62 50   62 1 154  
  62 50       195  
170 62 50 33 62 1 280 # META
  62 50       139  
  62         201  
171 62 50 33 62 1 235 has 'meta.generator' => '', coerce => \&to_string;
  62 50       140  
  62         213  
172 62 50 33 62 1 124 has 'meta.header_hash' => '', coerce => \&to_string;
  62 50       238  
  62         5919  
173 62 50 50 62 1 154 has 'meta.database_name' => '', coerce => \&to_string;
  62 50       179  
  62         206  
174 62 50 33 62 1 166 has 'meta.database_name_changed' => sub { gmtime }, coerce => \&to_time;
  62 50       142  
  62         3913  
175 62 50 50 62 1 130 has 'meta.database_description' => '', coerce => \&to_string;
  62 50       157  
  62         211  
176 62 50 33 62 1 170 has 'meta.database_description_changed' => sub { gmtime }, coerce => \&to_time;
  62 50       148  
  62         3802  
177 62 50 50 62 1 162 has 'meta.default_username' => '', coerce => \&to_string;
  62 50       159  
  62         214  
178 62 50 33 62 1 169 has 'meta.default_username_changed' => sub { gmtime }, coerce => \&to_time;
  62 50       148  
  62         205  
179 62 50 33 62 1 152 has 'meta.maintenance_history_days' => HISTORY_DEFAULT_MAX_AGE, coerce => \&to_number;
  62 50       155  
  62         3690  
180 62 50 50 62 1 133 has 'meta.color' => '', coerce => \&to_string;
  62 50       171  
  62         197  
181 62 50 33     158 has 'meta.master_key_changed' => sub { gmtime }, coerce => \&to_time;
  62         141  
182 62 50 33 226 1 151 has 'meta.master_key_change_rec' => -1, coerce => \&to_number;
  226         638  
183 226 50   69 1 475 has 'meta.master_key_change_force' => -1, coerce => \&to_number;
  69 50       1403  
184 226 100 100 69 1 491 # has 'meta.memory_protection' => {};
  69 50       175  
  69         613  
185 69 100 66 62 1 157 has 'meta.custom_icons' => [];
  69 50       182  
  62         251  
186 69 50 66 62 1 145 has 'meta.recycle_bin_enabled' => true, coerce => \&to_bool;
  62 50       160  
  62         3804  
187 62 50 50 62 1 132 has 'meta.recycle_bin_uuid' => UUID_NULL, coerce => \&to_uuid;
  62 50       213  
  62         207  
188 62 50 33 62 1 154 has 'meta.recycle_bin_changed' => sub { gmtime }, coerce => \&to_time;
  62 50       145  
  62         3612  
189 62 50 50 62 1 139 has 'meta.entry_templates_group' => UUID_NULL, coerce => \&to_uuid;
  62 50       192  
  62         194  
190 62 50 33 64 1 154 has 'meta.entry_templates_group_changed' => sub { gmtime }, coerce => \&to_time;
  62 50       158  
  64         197  
191 62 50 33 64 1 125 has 'meta.last_selected_group' => UUID_NULL, coerce => \&to_uuid;
  64 50       154  
  64         201  
192 64 50 66 62 1 134 has 'meta.last_top_visible_group' => UUID_NULL, coerce => \&to_uuid;
  64 50       144  
  62         234  
193 64 50 66     146 has 'meta.history_max_items' => HISTORY_DEFAULT_MAX_ITEMS, coerce => \&to_number;
  62         143  
194 62   50     123 has 'meta.history_max_size' => HISTORY_DEFAULT_MAX_SIZE, coerce => \&to_number;
195             has 'meta.settings_changed' => sub { gmtime }, coerce => \&to_time;
196 62 50   62 1 3657 # has 'meta.binaries' => {};
197 62 50   62 1 199 # has 'meta.custom_data' => {};
  62 50       497  
198 62 50 33 62 1 176  
  62 50       152  
  62         455  
199 62 50 33 62 1 129 has 'memory_protection.protect_title' => false, coerce => \&to_bool;
  62 50       144  
  62         496  
200 62 50 33 62 1 125 has 'memory_protection.protect_username' => false, coerce => \&to_bool;
  62 50       144  
  62         448  
201 62 50 33     131 has 'memory_protection.protect_password' => true, coerce => \&to_bool;
  62         156  
202 62   33     124 has 'memory_protection.protect_url' => false, coerce => \&to_bool;
203             has 'memory_protection.protect_notes' => false, coerce => \&to_bool;
204             # has 'memory_protection.auto_enable_visual_hiding' => false;
205              
206             my @ATTRS = (
207             HEADER_TRANSFORM_SEED,
208             HEADER_TRANSFORM_ROUNDS,
209             HEADER_INNER_RANDOM_STREAM_KEY,
210             HEADER_INNER_RANDOM_STREAM_ID,
211             HEADER_PUBLIC_CUSTOM_DATA,
212             );
213             sub _set_nonlazy_attributes {
214 62     62   95 my $self = shift;
215 62         208 $self->$_ for list_attributes(ref $self), @ATTRS;
216             }
217              
218              
219             sub memory_protection {
220 425     425 1 547 my $self = shift;
221 425 50 66     895 $self->{meta}{memory_protection} = shift if @_ == 1 && is_plain_hashref($_[0]);
222 425 100 100     1835 return $self->{meta}{memory_protection} //= {} if !@_;
223              
224 115         133 my $string_key = shift;
225 115         219 my $key = 'protect_' . lc($string_key);
226              
227 115 50       196 $self->meta->{memory_protection}{$key} = shift if @_;
228 115         205 $self->meta->{memory_protection}{$key};
229             }
230              
231              
232             sub minimum_version {
233 122     122 1 245 my $self = shift;
234              
235             return KDBX_VERSION_4_1 if any {
236             nonempty $_->{last_modification_time}
237 122 50   3   444 } values %{$self->custom_data};
  3         11  
  122         270  
238              
239             return KDBX_VERSION_4_1 if any {
240             nonempty $_->{name} || nonempty $_->{last_modification_time}
241 122 100 100 11   555 } @{$self->custom_icons};
  11         40  
  122         259  
242              
243             return KDBX_VERSION_4_1 if $self->groups->next(sub {
244             nonempty $_->previous_parent_group ||
245             nonempty $_->tags ||
246 159   100 159   339 (any { nonempty $_->{last_modification_time} } values %{$_->custom_data})
247 120 100       447 });
248              
249             return KDBX_VERSION_4_1 if $self->entries(history => 1)->next(sub {
250             nonempty $_->previous_parent_group ||
251             (defined $_->quality_check && !$_->quality_check) ||
252 27   100 27   85 (any { nonempty $_->{last_modification_time} } values %{$_->custom_data})
253 117 100       888 });
254              
255 114 100       1291 return KDBX_VERSION_4_0 if $self->kdf->uuid ne KDF_UUID_AES;
256              
257 103 100       400 return KDBX_VERSION_4_0 if nonempty $self->public_custom_data;
258              
259             return KDBX_VERSION_4_0 if $self->objects->next(sub {
260 148     148   344 nonempty $_->custom_data
261 99 100       308 });
262              
263 88         936 return KDBX_VERSION_3_1;
264             }
265              
266             ##############################################################################
267              
268              
269             sub root {
270 678     678 1 64843 my $self = shift;
271 678 100       1116 if (@_) {
272 21         66 $self->{root} = $self->_wrap_group(@_);
273 21         65 $self->{root}->kdbx($self);
274             }
275 678   66     1368 $self->{root} //= $self->_implicit_root;
276 678         1343 return $self->_wrap_group($self->{root});
277             }
278              
279             # Called by File::KeePass::KDBX so that a File::KDBX an be treated as a File::KDBX::Group in that both types
280             # can have subgroups. File::KDBX already has a `groups' method that does something different from the
281             # File::KDBX::Groups `groups' method.
282             sub _kpx_groups {
283 0     0   0 my $self = shift;
284 0 0       0 return [] if !$self->{root};
285 0 0       0 return $self->_has_implicit_root ? $self->root->groups : [$self->root];
286             }
287              
288             sub _has_implicit_root {
289 3     3   11 my $self = shift;
290 3         7 my $root = $self->root;
291 3         10 my $temp = __PACKAGE__->_implicit_root;
292             # If an implicit root group has been changed in any significant way, it is no longer implicit.
293             return $root->name eq $temp->name &&
294             $root->is_expanded ^ $temp->is_expanded &&
295             $root->notes eq $temp->notes &&
296             !@{$root->entries} &&
297             !defined $root->custom_icon_uuid &&
298 3   33     11 !keys %{$root->custom_data} &&
299             $root->icon_id == $temp->icon_id &&
300             $root->expires ^ $temp->expires &&
301             $root->default_auto_type_sequence eq $temp->default_auto_type_sequence &&
302             !defined $root->enable_auto_type &&
303             !defined $root->enable_searching;
304             }
305              
306             sub _implicit_root {
307 65     65   93 my $self = shift;
308 65         3701 require File::KDBX::Group;
309 65 100       247 return File::KDBX::Group->new(
310             name => 'Root',
311             is_expanded => true,
312             notes => 'Added as an implicit root group by '.__PACKAGE__.'.',
313             ref $self ? (kdbx => $self) : (),
314             );
315             }
316              
317              
318             sub trace_lineage {
319 0     0 1 0 my $self = shift;
320 0         0 my $object = shift;
321 0         0 return $object->lineage(@_);
322             }
323              
324             sub _trace_lineage {
325 216     216   292 my $self = shift;
326 216         280 my $object = shift;
327 216         326 my @lineage = @_;
328              
329 216 100       478 push @lineage, $self->root if !@lineage;
330 216 50       468 my $base = $lineage[-1] or return [];
331              
332 216         394 my $uuid = $object->uuid;
333 216 100   170   591 return \@lineage if any { $_->uuid eq $uuid } @{$base->groups}, @{$base->entries};
  170         306  
  216         405  
  216         414  
334              
335 215         402 for my $subgroup (@{$base->groups}) {
  215         361  
336 90         186 my $result = $self->_trace_lineage($object, @lineage, $subgroup);
337 90 50       321 return $result if $result;
338             }
339             }
340              
341              
342             sub recycle_bin {
343 6     6 1 7 my $self = shift;
344 6 50       13 if (my $group = shift) {
345 0         0 $self->recycle_bin_uuid($group->uuid);
346 0         0 return $group;
347             }
348 6         8 my $group;
349 6         11 my $uuid = $self->recycle_bin_uuid;
350 6 100       17 $group = $self->groups->grep(uuid => $uuid)->next if $uuid ne UUID_NULL;
351 6 100 66     51 if (!$group && $self->recycle_bin_enabled) {
352 1         15 $group = $self->add_group(
353             name => 'Recycle Bin',
354             icon_id => ICON_TRASHCAN_FULL,
355             enable_auto_type => false,
356             enable_searching => false,
357             );
358 1         4 $self->recycle_bin_uuid($group->uuid);
359             }
360 6         18 return $group;
361             }
362              
363              
364             sub entry_templates {
365 0     0 1 0 my $self = shift;
366 0 0       0 if (my $group = shift) {
367 0         0 $self->entry_templates_group($group->uuid);
368 0         0 return $group;
369             }
370 0         0 my $uuid = $self->entry_templates_group;
371 0 0       0 return if $uuid eq UUID_NULL;
372 0         0 return $self->groups->grep(uuid => $uuid)->next;
373             }
374              
375              
376             sub last_selected {
377 0     0 1 0 my $self = shift;
378 0 0       0 if (my $group = shift) {
379 0         0 $self->last_selected_group($group->uuid);
380 0         0 return $group;
381             }
382 0         0 my $uuid = $self->last_selected_group;
383 0 0       0 return if $uuid eq UUID_NULL;
384 0         0 return $self->groups->grep(uuid => $uuid)->next;
385             }
386              
387              
388             sub last_top_visible {
389 0     0 1 0 my $self = shift;
390 0 0       0 if (my $group = shift) {
391 0         0 $self->last_top_visible_group($group->uuid);
392 0         0 return $group;
393             }
394 0         0 my $uuid = $self->last_top_visible_group;
395 0 0       0 return if $uuid eq UUID_NULL;
396 0         0 return $self->groups->grep(uuid => $uuid)->next;
397             }
398              
399             ##############################################################################
400              
401              
402             sub add_group {
403 15     15 1 76 my $self = shift;
404 15 50       45 my $group = @_ % 2 == 1 ? shift : undef;
405 15         44 my %args = @_;
406              
407             # find the right group to add the group to
408 15   33     57 my $parent = delete $args{group} // $self->root;
409 15 50       43 $parent = $self->groups->grep({uuid => $parent})->next if !ref $parent;
410 15 50       46 $parent or throw 'Invalid group';
411              
412 15 50       73 return $parent->add_group(defined $group ? $group : (), %args, kdbx => $self);
413             }
414              
415             sub _wrap_group {
416 699     699   903 my $self = shift;
417 699         787 my $group = shift;
418 699         2803 require File::KDBX::Group;
419 699         1984 return File::KDBX::Group->wrap($group, $self);
420             }
421              
422              
423             sub groups {
424 136     136 1 211 my $self = shift;
425 136 50       464 my %args = @_ % 2 == 0 ? @_ : (base => shift, @_);
426 136   33     473 my $base = delete $args{base} // $self->root;
427              
428 136         412 return $base->all_groups(%args);
429             }
430              
431             ##############################################################################
432              
433              
434             sub add_entry {
435 20     20 1 934 my $self = shift;
436 20 100       65 my $entry = @_ % 2 == 1 ? shift : undef;
437 20         66 my %args = @_;
438              
439             # find the right group to add the entry to
440 20   33     76 my $parent = delete $args{group} // $self->root;
441 20 50       59 $parent = $self->groups->grep({uuid => $parent})->next if !ref $parent;
442 20 50       51 $parent or throw 'Invalid group';
443              
444 20 100       96 return $parent->add_entry(defined $entry ? $entry : (), %args, kdbx => $self);
445             }
446              
447             sub _wrap_entry {
448 0     0   0 my $self = shift;
449 0         0 my $entry = shift;
450 0         0 require File::KDBX::Entry;
451 0         0 return File::KDBX::Entry->wrap($entry, $self);
452             }
453              
454              
455             sub entries {
456 209     209 1 1412 my $self = shift;
457 209 50       659 my %args = @_ % 2 == 0 ? @_ : (base => shift, @_);
458 209   33     678 my $base = delete $args{base} // $self->root;
459              
460 209         674 return $base->all_entries(%args);
461             }
462              
463             ##############################################################################
464              
465              
466             sub objects {
467 108     108 1 193 my $self = shift;
468 108 50       297 my %args = @_ % 2 == 0 ? @_ : (base => shift, @_);
469 108   33     338 my $base = delete $args{base} // $self->root;
470              
471 108         365 return $base->all_objects(%args);
472             }
473              
474 0     0   0 sub __iter__ { $_[0]->objects }
475              
476             ##############################################################################
477              
478              
479             sub custom_icon {
480 0     0 1 0 my $self = shift;
481 0 0       0 my %args = @_ == 2 ? (uuid => shift, data => shift)
    0          
482             : @_ % 2 == 1 ? (uuid => shift, @_) : @_;
483              
484 0 0 0     0 if (!$args{uuid} && !$args{data}) {
485 0         0 my %standard = (uuid => 1, data => 1, name => 1, last_modification_time => 1);
486 0         0 my @other_keys = grep { !$standard{$_} } keys %args;
  0         0  
487 0 0       0 if (@other_keys == 1) {
488 0         0 my $key = $args{key} = $other_keys[0];
489 0         0 $args{data} = delete $args{$key};
490             }
491             }
492              
493 0 0       0 my $uuid = $args{uuid} or throw 'Must provide a custom icon UUID to access';
494 0   0 0   0 my $icon = (first { $_->{uuid} eq $uuid } @{$self->custom_icons}) // do {
  0         0  
  0         0  
495 0         0 push @{$self->custom_icons}, my $i = { uuid => $uuid };
  0         0  
496 0         0 $i;
497             };
498              
499 0         0 my $fields = \%args;
500 0 0       0 $fields = $args{data} if is_plain_hashref($args{data});
501              
502 0         0 while (my ($field, $value) = each %$fields) {
503 0         0 $icon->{$field} = $value;
504             }
505 0         0 return $icon;
506             }
507              
508              
509             sub custom_icon_data {
510 5     5 1 8 my $self = shift;
511 5   100     12 my $uuid = shift // return;
512 4 50   4   13 my $icon = first { $_->{uuid} eq $uuid } @{$self->custom_icons} or return;
  4         13  
  4         8  
513 4         17 return $icon->{data};
514             }
515              
516              
517             sub add_custom_icon {
518 9     9 1 15 my $self = shift;
519 9 50       37 my %args = @_ % 2 == 1 ? (data => shift, @_) : @_;
520              
521 9 50       22 defined $args{data} or throw 'Must provide image data';
522              
523 9   33     36 my $uuid = $args{uuid} // generate_uuid;
524 9         19 push @{$self->custom_icons}, {
525             @_,
526             uuid => $uuid,
527             data => $args{data},
528 9         16 };
529 9         23 return $uuid;
530             }
531              
532              
533             sub remove_custom_icon {
534 5     5 1 10 my $self = shift;
535 5         7 my $uuid = shift;
536 5         7 my @deleted;
537 5 100       9 @{$self->custom_icons} = grep { $_->{uuid} eq $uuid ? do { push @deleted, $_; 0 } : 1 }
  12         29  
  5         8  
  5         10  
538 5         6 @{$self->custom_icons};
  5         12  
539 5 50       21 $self->add_deleted_object($uuid) if @deleted;
540 5         18 return @deleted;
541             }
542              
543             ##############################################################################
544              
545              
546             sub custom_data {
547 123     123 1 182 my $self = shift;
548 123 50 33     282 $self->{meta}{custom_data} = shift if @_ == 1 && is_plain_hashref($_[0]);
549 123 100 100     890 return $self->{meta}{custom_data} //= {} if !@_;
550              
551 1 0       7 my %args = @_ == 2 ? (key => shift, value => shift)
    50          
552             : @_ % 2 == 1 ? (key => shift, @_) : @_;
553              
554 1 0 33     4 if (!$args{key} && !$args{value}) {
555 0         0 my %standard = (key => 1, value => 1, last_modification_time => 1);
556 0         0 my @other_keys = grep { !$standard{$_} } keys %args;
  0         0  
557 0 0       0 if (@other_keys == 1) {
558 0         0 my $key = $args{key} = $other_keys[0];
559 0         0 $args{value} = delete $args{$key};
560             }
561             }
562              
563 1 50       5 my $key = $args{key} or throw 'Must provide a custom_data key to access';
564              
565 1 50       4 return $self->{meta}{custom_data}{$key} = $args{value} if is_plain_hashref($args{value});
566              
567 1         63 while (my ($field, $value) = each %args) {
568 2         10 $self->{meta}{custom_data}{$key}{$field} = $value;
569             }
570 1         3 return $self->{meta}{custom_data}{$key};
571             }
572              
573              
574             sub custom_data_value {
575 0     0 1 0 my $self = shift;
576 0   0     0 my $data = $self->custom_data(@_) // return;
577 0         0 return $data->{value};
578             }
579              
580              
581             sub public_custom_data {
582 177     177 1 1380 my $self = shift;
583 177 50 33     401 $self->{headers}{+HEADER_PUBLIC_CUSTOM_DATA} = shift if @_ == 1 && is_plain_hashref($_[0]);
584 177 50 100     877 return $self->{headers}{+HEADER_PUBLIC_CUSTOM_DATA} //= {} if !@_;
585              
586 0 0       0 my $key = shift or throw 'Must provide a public_custom_data key to access';
587 0 0       0 $self->{headers}{+HEADER_PUBLIC_CUSTOM_DATA}{$key} = shift if @_;
588 0         0 return $self->{headers}{+HEADER_PUBLIC_CUSTOM_DATA}{$key};
589             }
590              
591             ##############################################################################
592              
593             # TODO
594              
595             # sub merge_to {
596             # my $self = shift;
597             # my $other = shift;
598             # my %options = @_; # prefer_old / prefer_new
599             # $other->merge_from($self);
600             # }
601              
602             # sub merge_from {
603             # my $self = shift;
604             # my $other = shift;
605              
606             # die 'Not implemented';
607             # }
608              
609              
610             sub add_deleted_object {
611 11     11 1 17 my $self = shift;
612 11         15 my $uuid = shift;
613              
614             # ignore null and meta stream UUIDs
615 11 50 33     42 return if $uuid eq UUID_NULL || $uuid eq '0' x 16;
616              
617 11         33 $self->deleted_objects->{$uuid} = {
618             uuid => $uuid,
619             deletion_time => scalar gmtime,
620             };
621             }
622              
623              
624             sub remove_deleted_object {
625 49     49 1 69 my $self = shift;
626 49         63 my $uuid = shift;
627 49         124 delete $self->deleted_objects->{$uuid};
628             }
629              
630              
631             sub clear_deleted_objects {
632 0     0 1 0 my $self = shift;
633 0         0 %{$self->deleted_objects} = ();
  0         0  
634             }
635              
636             ##############################################################################
637              
638              
639             sub resolve_reference {
640 18     18 1 6190 my $self = shift;
641 18   50     38 my $wanted = shift // return;
642 18         27 my $search_in = shift;
643 18         19 my $text = shift;
644              
645 18 100       33 if (!defined $text) {
646 17         85 $wanted =~ s/^\{REF:([^\}]+)\}$/$1/i;
647 17         91 ($wanted, $search_in, $text) = $wanted =~ /^([TUPANI])\@([TUPANIO]):(.*)$/i;
648             }
649 18 50 66     96 $wanted && $search_in && nonempty($text) or return;
      66        
650              
651 17         83 my %fields = (
652             T => 'expand_title',
653             U => 'expand_username',
654             P => 'expand_password',
655             A => 'expand_url',
656             N => 'expand_notes',
657             I => 'uuid',
658             O => 'other_strings',
659             );
660 17 50       41 $wanted = $fields{$wanted} or return;
661 17 50       41 $search_in = $fields{$search_in} or return;
662              
663 17 100       69 my $query = $search_in eq 'uuid' ? query($search_in => uuid($text))
664             : simple_expression_query($text, '=~', $search_in);
665              
666 17         52 my $entry = $self->entries->grep($query)->next;
667 17 100       279 $entry or return;
668              
669 15         52 return $entry->$wanted;
670             }
671              
672             our %PLACEHOLDERS = (
673             # 'PLACEHOLDER' => sub { my ($entry, $arg) = @_; ... };
674             'TITLE' => sub { $_[0]->expand_title },
675             'USERNAME' => sub { $_[0]->expand_username },
676             'PASSWORD' => sub { $_[0]->expand_password },
677             'NOTES' => sub { $_[0]->expand_notes },
678             'S:' => sub { $_[0]->string_value($_[1]) },
679             'URL' => sub { $_[0]->expand_url },
680             'URL:RMVSCM' => sub { local $_ = $_[0]->url; s!^[^:/\?\#]+://!!; $_ },
681             'URL:WITHOUTSCHEME' => sub { local $_ = $_[0]->url; s!^[^:/\?\#]+://!!; $_ },
682             'URL:SCM' => sub { (split_url($_[0]->url))[0] },
683             'URL:SCHEME' => sub { (split_url($_[0]->url))[0] }, # non-standard
684             'URL:HOST' => sub { (split_url($_[0]->url))[2] },
685             'URL:PORT' => sub { (split_url($_[0]->url))[3] },
686             'URL:PATH' => sub { (split_url($_[0]->url))[4] },
687             'URL:QUERY' => sub { (split_url($_[0]->url))[5] },
688             'URL:HASH' => sub { (split_url($_[0]->url))[6] }, # non-standard
689             'URL:FRAGMENT' => sub { (split_url($_[0]->url))[6] }, # non-standard
690             'URL:USERINFO' => sub { (split_url($_[0]->url))[1] },
691             'URL:USERNAME' => sub { (split_url($_[0]->url))[7] },
692             'URL:PASSWORD' => sub { (split_url($_[0]->url))[8] },
693             'UUID' => sub { local $_ = format_uuid($_[0]->uuid); s/-//g; $_ },
694             'REF:' => sub { $_[0]->kdbx->resolve_reference($_[1]) },
695             'INTERNETEXPLORER' => sub { load_optional('IPC::Cmd'); IPC::Cmd::can_run('iexplore') },
696             'FIREFOX' => sub { load_optional('IPC::Cmd'); IPC::Cmd::can_run('firefox') },
697             'GOOGLECHROME' => sub { load_optional('IPC::Cmd'); IPC::Cmd::can_run('google-chrome') },
698             'OPERA' => sub { load_optional('IPC::Cmd'); IPC::Cmd::can_run('opera') },
699             'SAFARI' => sub { load_optional('IPC::Cmd'); IPC::Cmd::can_run('safari') },
700             'APPDIR' => sub { load_optional('FindBin'); $FindBin::Bin },
701             'GROUP' => sub { my $p = $_[0]->group; $p ? $p->name : undef },
702             'GROUP_PATH' => sub { $_[0]->path },
703             'GROUP_NOTES' => sub { my $p = $_[0]->group; $p ? $p->notes : undef },
704             # 'GROUP_SEL'
705             # 'GROUP_SEL_PATH'
706             # 'GROUP_SEL_NOTES'
707             # 'DB_PATH'
708             # 'DB_DIR'
709             # 'DB_NAME'
710             # 'DB_BASENAME'
711             # 'DB_EXT'
712             'ENV:' => sub { $ENV{$_[1]} },
713             'ENV_DIRSEP' => sub { load_optional('File::Spec')->catfile('', '') },
714             'ENV_PROGRAMFILES_X86' => sub { $ENV{'ProgramFiles(x86)'} || $ENV{'ProgramFiles'} },
715             # 'T-REPLACE-RX:'
716             # 'T-CONV:'
717             'DT_SIMPLE' => sub { localtime->strftime('%Y%m%d%H%M%S') },
718             'DT_YEAR' => sub { localtime->strftime('%Y') },
719             'DT_MONTH' => sub { localtime->strftime('%m') },
720             'DT_DAY' => sub { localtime->strftime('%d') },
721             'DT_HOUR' => sub { localtime->strftime('%H') },
722             'DT_MINUTE' => sub { localtime->strftime('%M') },
723             'DT_SECOND' => sub { localtime->strftime('%S') },
724             'DT_UTC_SIMPLE' => sub { gmtime->strftime('%Y%m%d%H%M%S') },
725             'DT_UTC_YEAR' => sub { gmtime->strftime('%Y') },
726             'DT_UTC_MONTH' => sub { gmtime->strftime('%m') },
727             'DT_UTC_DAY' => sub { gmtime->strftime('%d') },
728             'DT_UTC_HOUR' => sub { gmtime->strftime('%H') },
729             'DT_UTC_MINUTE' => sub { gmtime->strftime('%M') },
730             'DT_UTC_SECOND' => sub { gmtime->strftime('%S') },
731             # 'PICKCHARS'
732             # 'PICKCHARS:'
733             # 'PICKFIELD'
734             # 'NEWPASSWORD'
735             # 'NEWPASSWORD:'
736             # 'PASSWORD_ENC'
737             'HMACOTP' => sub { $_[0]->hmac_otp },
738             'TIMEOTP' => sub { $_[0]->time_otp },
739             'C:' => sub { '' }, # comment
740             # 'BASE'
741             # 'BASE:'
742             # 'CLIPBOARD'
743             # 'CLIPBOARD-SET:'
744             # 'CMD:'
745             );
746              
747             ##############################################################################
748              
749              
750             sub _safe {
751 54     54   87 my $self = shift;
752 54 100       164 $SAFE{$self} = shift if @_;
753 54         248 $SAFE{$self};
754             }
755              
756 94     94   426 sub _remove_safe { delete $SAFE{$_[0]} }
757              
758             sub lock {
759 2     2 1 128 my $self = shift;
760              
761             # Find things to lock:
762 2         3 my @strings;
763             $self->entries(history => 1)->each(sub {
764 1     1   3 my $strings = $_->strings;
765 1         3 for my $string_key (keys %$strings) {
766 6         46 my $string = $strings->{$string_key};
767 6 100 66     13 push @strings, $string if $string->{protect} // $self->memory_protection($string_key);
768             }
769 1         1 push @strings, grep { $_->{protect} } values %{$_->binaries};
  2         5  
  1         3  
770 2         5 });
771 2 100       27 return $self if !@strings; # nothing to do
772              
773 1 50       3 if (my $safe = $self->_safe) {
774 0         0 $safe->add(\@strings);
775             }
776             else {
777 1         7 $self->_safe(File::KDBX::Safe->new(\@strings));
778             }
779 1         3 return $self;
780             }
781              
782              
783             sub unlock {
784 21     21 1 11049 my $self = shift;
785 21 100       72 my $safe = $self->_safe or return $self;
786              
787 5         27 $safe->unlock;
788 5         19 $self->_remove_safe;
789              
790 5         19 return $self;
791             }
792              
793              
794             sub unlock_scoped {
795 15 50   15 1 60 throw 'Programmer error: Cannot call unlock_scoped in void context' if !defined wantarray;
796 15         30 my $self = shift;
797 15 50       49 return if !$self->is_locked;
798 0         0 require Scope::Guard;
799 0     0   0 my $guard = Scope::Guard->new(sub { $self->lock });
  0         0  
800 0         0 $self->unlock;
801 0         0 return $guard;
802             }
803              
804              
805             sub peek {
806 4     4 1 7 my $self = shift;
807 4         7 my $string = shift;
808 4 50       8 my $safe = $self->_safe or return;
809 4         18 return $safe->peek($string);
810             }
811              
812              
813 15     15 1 46 sub is_locked { !!$_[0]->_safe }
814              
815             ##############################################################################
816              
817             # sub check {
818             # - Fixer tool. Can repair inconsistencies, including:
819             # - Orphaned binaries... not really a thing anymore since we now distribute binaries amongst entries
820             # - Unused custom icons (OFF, data loss)
821             # - Duplicate icons
822             # - All data types are valid
823             # - date times are correct
824             # - boolean fields
825             # - All UUIDs refer to things that exist
826             # - previous parent group
827             # - recycle bin
828             # - last selected group
829             # - last visible group
830             # - Enforce history size limits (ON)
831             # - Check headers/meta (ON)
832             # - Duplicate deleted objects (ON)
833             # - Duplicate window associations (OFF)
834             # - Header UUIDs match known ciphers/KDFs?
835             # }
836              
837              
838             sub remove_empty_groups {
839 1     1 1 6 my $self = shift;
840 1         2 my @removed;
841             $self->groups(algorithm => 'dfs')
842             ->where(-true => 'is_empty')
843 1     3   4 ->each(sub { push @removed, $_->remove });
  3         10  
844 1         34 return @removed;
845             }
846              
847              
848             sub remove_unused_icons {
849 2     2 1 5 my $self = shift;
850 2         3 my %icons = map { $_->{uuid} => 0 } @{$self->custom_icons};
  3         8  
  2         4  
851              
852 2   100 6   6 $self->objects->each(sub { ++$icons{$_->custom_icon_uuid // ''} });
  6         15  
853              
854 2         20 my @removed;
855 2         6 push @removed, $self->remove_custom_icon($_) for grep { $icons{$_} == 0 } keys %icons;
  5         16  
856 2         12 return @removed;
857             }
858              
859              
860             sub remove_duplicate_icons {
861 1     1 1 6 my $self = shift;
862              
863 1         3 my %seen;
864             my %dup;
865 1         2 for my $icon (@{$self->custom_icons}) {
  1         4  
866 3         20 my $digest = digest_data('SHA256', $icon->{data});
867 3 100       8 if (my $other = $seen{$digest}) {
868 1         4 $dup{$icon->{uuid}} = $other->{uuid};
869             }
870             else {
871 2         5 $seen{$digest} = $icon;
872             }
873             }
874              
875 1         2 my @removed;
876 1         6 while (my ($old_uuid, $new_uuid) = each %dup) {
877             $self->objects
878             ->where(custom_icon_uuid => $old_uuid)
879 1     1   4 ->each(sub { $_->custom_icon_uuid($new_uuid) });
  1         4  
880 1         17 push @removed, $self->remove_custom_icon($old_uuid);
881             }
882 1         8 return @removed;
883             }
884              
885              
886             sub prune_history {
887 2     2 1 18 my $self = shift;
888 2         6 my %args = @_;
889              
890 2   33     9 my $max_items = $args{max_items} // $self->history_max_items // HISTORY_DEFAULT_MAX_ITEMS;
      50        
891 2   33     8 my $max_size = $args{max_size} // $self->history_max_size // HISTORY_DEFAULT_MAX_SIZE;
      50        
892 2   33     6 my $max_age = $args{max_age} // $self->maintenance_history_days // HISTORY_DEFAULT_MAX_AGE;
      50        
893              
894 2         2 my @removed;
895             $self->entries->each(sub {
896 2     2   9 push @removed, $_->prune_history(
897             max_items => $max_items,
898             max_size => $max_size,
899             max_age => $max_age,
900             );
901 2         5 });
902 2         29 return @removed;
903             }
904              
905              
906             sub randomize_seeds {
907 14     14 1 26 my $self = shift;
908 14         26 my $iv_size = 16;
909 14 100       30 $iv_size = $self->cipher(key => "\0" x 32)->iv_size if KDBX_VERSION_4_0 <= $self->version;
910 14         53 $self->encryption_iv(random_bytes($iv_size));
911 14         40 $self->inner_random_stream_key(random_bytes(64));
912 14         41 $self->master_seed(random_bytes(32));
913 14         49 $self->stream_start_bytes(random_bytes(32));
914 14         39 $self->transform_seed(random_bytes(32));
915             }
916              
917             ##############################################################################
918              
919              
920             sub key {
921 36     36 1 64 my $self = shift;
922 36 50       197 $KEYS{$self} = File::KDBX::Key->new(@_) if @_;
923 36         103 $KEYS{$self};
924             }
925              
926              
927             sub composite_key {
928 45     45 1 81 my $self = shift;
929 45         2199 require File::KDBX::Key::Composite;
930 45         312 return File::KDBX::Key::Composite->new(@_);
931             }
932              
933              
934             sub kdf {
935 176     176 1 2634 my $self = shift;
936 176 50       429 my %args = @_ % 2 == 1 ? (params => shift, @_) : @_;
937              
938 176         285 my $params = $args{params};
939              
940 176   33     620 $params //= $self->kdf_parameters;
941 176 50       225 $params = {%{$params || {}}};
  176         891  
942              
943 176 50 33     841 if (empty $params || !defined $params->{+KDF_PARAM_UUID}) {
944 0         0 $params->{+KDF_PARAM_UUID} = KDF_UUID_AES;
945             }
946 176 100       447 if ($params->{+KDF_PARAM_UUID} eq KDF_UUID_AES) {
947             # AES_CHALLENGE_RESPONSE is equivalent to AES if there are no challenge-response keys, and since
948             # non-KeePassXC implementations don't support challenge-response keys anyway, there's no problem with
949             # always using AES_CHALLENGE_RESPONSE for all KDBX4+ databases.
950             # For compatibility, we should not *write* AES_CHALLENGE_RESPONSE, but the dumper handles that.
951 158 100       612 if ($self->version >= KDBX_VERSION_4_0) {
952 20         35 $params->{+KDF_PARAM_UUID} = KDF_UUID_AES_CHALLENGE_RESPONSE;
953             }
954 158   33     304 $params->{+KDF_PARAM_AES_SEED} //= $self->transform_seed;
955 158   33     316 $params->{+KDF_PARAM_AES_ROUNDS} //= $self->transform_rounds;
956             }
957              
958 176         700 require File::KDBX::KDF;
959 176         871 return File::KDBX::KDF->new(%$params);
960             }
961              
962             sub transform_seed {
963 85     85 1 7955 my $self = shift;
964 85         135 my $param = KDF_PARAM_AES_SEED; # Short cut: Argon2 uses the same parameter name ("S")
965             $self->headers->{+HEADER_TRANSFORM_SEED} =
966 85 100       203 $self->headers->{+HEADER_KDF_PARAMETERS}{$param} = shift if @_;
967             $self->headers->{+HEADER_TRANSFORM_SEED} =
968 85   33     188 $self->headers->{+HEADER_KDF_PARAMETERS}{$param} //= random_bytes(32);
969             }
970              
971             sub transform_rounds {
972 75     75 1 124 my $self = shift;
973 75         4362 require File::KDBX::KDF;
974 75   50     238 my $info = $File::KDBX::KDF::ROUNDS_INFO{$self->kdf_parameters->{+KDF_PARAM_UUID} // ''} //
      66        
975             $File::KDBX::KDF::DEFAULT_ROUNDS_INFO;
976             $self->headers->{+HEADER_TRANSFORM_ROUNDS} =
977 75 100       193 $self->headers->{+HEADER_KDF_PARAMETERS}{$info->{p}} = shift if @_;
978             $self->headers->{+HEADER_TRANSFORM_ROUNDS} =
979 75   66     161 $self->headers->{+HEADER_KDF_PARAMETERS}{$info->{p}} //= $info->{d};
980             }
981              
982              
983             sub cipher {
984 46     46 1 77 my $self = shift;
985 46         134 my %args = @_;
986              
987 46   33     240 $args{uuid} //= $self->cipher_id;
988 46   33     227 $args{iv} //= $self->encryption_iv;
989              
990 46         169 require File::KDBX::Cipher;
991 46         242 return File::KDBX::Cipher->new(%args);
992             }
993              
994              
995             sub random_stream {
996 18     18 1 31 my $self = shift;
997 18         44 my %args = @_;
998              
999 18   33     189 $args{stream_id} //= delete $args{id} // $self->inner_random_stream_id;
      33        
1000 18   33     104 $args{key} //= $self->inner_random_stream_key;
1001              
1002 18         105 require File::KDBX::Cipher;
1003 18         188 File::KDBX::Cipher->new(%args);
1004             }
1005              
1006             sub inner_random_stream_id {
1007 88     88 1 835 my $self = shift;
1008             $self->inner_headers->{+INNER_HEADER_INNER_RANDOM_STREAM_ID}
1009 88 100       193 = $self->headers->{+HEADER_INNER_RANDOM_STREAM_ID} = shift if @_;
1010             $self->inner_headers->{+INNER_HEADER_INNER_RANDOM_STREAM_ID}
1011 88   66     195 //= $self->headers->{+HEADER_INNER_RANDOM_STREAM_ID} //= do {
      66        
1012 62         170 my $version = $self->minimum_version;
1013 62 100       441 $version < KDBX_VERSION_4_0 ? STREAM_ID_SALSA20 : STREAM_ID_CHACHA20;
1014             };
1015             }
1016              
1017             sub inner_random_stream_key {
1018 110     110 1 252 my $self = shift;
1019 110 100       238 if (@_) {
1020             # These are probably the same SvPV so erasing one will CoW, but erasing the second should do the
1021             # trick anyway.
1022 22         51 erase \$self->inner_headers->{+INNER_HEADER_INNER_RANDOM_STREAM_KEY};
1023 22         57 erase \$self->headers->{+HEADER_INNER_RANDOM_STREAM_KEY};
1024             $self->inner_headers->{+INNER_HEADER_INNER_RANDOM_STREAM_KEY}
1025 22         57 = $self->headers->{+HEADER_INNER_RANDOM_STREAM_KEY} = shift;
1026             }
1027             $self->inner_headers->{+INNER_HEADER_INNER_RANDOM_STREAM_KEY}
1028 110   66     202 //= $self->headers->{+HEADER_INNER_RANDOM_STREAM_KEY} //= random_bytes(64); # 32
      66        
1029             }
1030              
1031             #########################################################################################
1032              
1033             sub _handle_signal {
1034 69     69   100 my $self = shift;
1035 69         83 my $object = shift;
1036 69         89 my $type = shift;
1037              
1038 69         351 my %handlers = (
1039             'entry.added' => \&_handle_object_added,
1040             'group.added' => \&_handle_object_added,
1041             'entry.removed' => \&_handle_object_removed,
1042             'group.removed' => \&_handle_object_removed,
1043             'entry.uuid.changed' => \&_handle_entry_uuid_changed,
1044             'group.uuid.changed' => \&_handle_group_uuid_changed,
1045             );
1046 69 50       193 my $handler = $handlers{$type} or return;
1047 69         159 $self->$handler($object, @_);
1048             }
1049              
1050             sub _handle_object_added {
1051 49     49   64 my $self = shift;
1052 49         63 my $object = shift;
1053 49         109 $self->remove_deleted_object($object->uuid);
1054             }
1055              
1056             sub _handle_object_removed {
1057 6     6   7 my $self = shift;
1058 6         9 my $object = shift;
1059 6   50     14 my $old_uuid = $object->{uuid} // return;
1060              
1061 6         15 my $meta = $self->meta;
1062 6 50 50     20 $self->recycle_bin_uuid(UUID_NULL) if $old_uuid eq ($meta->{recycle_bin_uuid} // '');
1063 6 50 50     19 $self->entry_templates_group(UUID_NULL) if $old_uuid eq ($meta->{entry_templates_group} // '');
1064 6 50 50     27 $self->last_selected_group(UUID_NULL) if $old_uuid eq ($meta->{last_selected_group} // '');
1065 6 50 50     29 $self->last_top_visible_group(UUID_NULL) if $old_uuid eq ($meta->{last_top_visible_group} // '');
1066              
1067 6         15 $self->add_deleted_object($old_uuid);
1068             }
1069              
1070             sub _handle_entry_uuid_changed {
1071 13     13   17 my $self = shift;
1072 13         17 my $object = shift;
1073 13         17 my $new_uuid = shift;
1074 13   50     28 my $old_uuid = shift // return;
1075              
1076 13         31 my $old_pretty = format_uuid($old_uuid);
1077 13         27 my $new_pretty = format_uuid($new_uuid);
1078 13         171 my $fieldref_match = qr/\{REF:([TUPANI])\@I:\Q$old_pretty\E\}/is;
1079              
1080             $self->entries->each(sub {
1081 26 50 50 26   82 $_->previous_parent_group($new_uuid) if $old_uuid eq ($_->{previous_parent_group} // '');
1082              
1083 26         32 for my $string (values %{$_->strings}) {
  26         48  
1084 130 100 66     422 next if !defined $string->{value} || $string->{value} !~ $fieldref_match;
1085 1         4 my $txn = $_->begin_work;
1086 1         13 $string->{value} =~ s/$fieldref_match/{REF:$1\@I:$new_pretty}/g;
1087 1         4 $txn->commit;
1088             }
1089 13         36 });
1090             }
1091              
1092             sub _handle_group_uuid_changed {
1093 1     1   2 my $self = shift;
1094 1         3 my $object = shift;
1095 1         2 my $new_uuid = shift;
1096 1   50     4 my $old_uuid = shift // return;
1097              
1098 1         5 my $meta = $self->meta;
1099 1 50 50     6 $self->recycle_bin_uuid($new_uuid) if $old_uuid eq ($meta->{recycle_bin_uuid} // '');
1100 1 50 50     6 $self->entry_templates_group($new_uuid) if $old_uuid eq ($meta->{entry_templates_group} // '');
1101 1 50 50     6 $self->last_selected_group($new_uuid) if $old_uuid eq ($meta->{last_selected_group} // '');
1102 1 50 50     5 $self->last_top_visible_group($new_uuid) if $old_uuid eq ($meta->{last_top_visible_group} // '');
1103              
1104             $self->groups->each(sub {
1105 2 50 50 2   11 $_->last_top_visible_entry($new_uuid) if $old_uuid eq ($_->{last_top_visible_entry} // '');
1106 2 50 50     12 $_->previous_parent_group($new_uuid) if $old_uuid eq ($_->{previous_parent_group} // '');
1107 1         5 });
1108             $self->entries->each(sub {
1109 3 50 50 3   15 $_->previous_parent_group($new_uuid) if $old_uuid eq ($_->{previous_parent_group} // '');
1110 1         10 });
1111             }
1112              
1113             #########################################################################################
1114              
1115              
1116             #########################################################################################
1117              
1118 0     0 0   sub TO_JSON { +{%{$_[0]}} }
  0            
1119              
1120             1;
1121              
1122             __END__