File Coverage

blib/lib/File/KDBX/Object.pm
Criterion Covered Total %
statement 284 300 94.6
branch 98 128 76.5
condition 77 106 72.6
subroutine 48 54 88.8
pod 21 24 87.5
total 528 612 86.2


line stmt bran cond sub pod time code
1             package File::KDBX::Object;
2             # ABSTRACT: A KDBX database object
3              
4 11     11   6710 use warnings;
  11         22  
  11         360  
5 11     11   53 use strict;
  11         25  
  11         204  
6              
7 11     11   42 use Devel::GlobalDestruction;
  11         23  
  11         59  
8 11     11   619 use File::KDBX::Constants qw(:bool);
  11         20  
  11         992  
9 11     11   63 use File::KDBX::Error;
  11         19  
  11         406  
10 11     11   51 use File::KDBX::Util qw(:uuid);
  11         15  
  11         940  
11 11     11   57 use Hash::Util::FieldHash qw(fieldhashes);
  11         25  
  11         494  
12 11     11   55 use List::Util qw(any first);
  11         23  
  11         589  
13 11     11   59 use Ref::Util qw(is_arrayref is_plain_arrayref is_plain_hashref is_ref);
  11         18  
  11         535  
14 11     11   56 use Scalar::Util qw(blessed weaken);
  11         18  
  11         479  
15 11     11   56 use namespace::clean;
  11         22  
  11         56  
16              
17             our $VERSION = '0.906'; # VERSION
18              
19             fieldhashes \my (%KDBX, %PARENT, %TXNS, %REFS, %SIGNALS);
20              
21              
22             sub new {
23 241     241 1 115842 my $class = shift;
24              
25             # copy constructor
26 241 50 100     698 return $_[0]->clone if @_ == 1 && blessed $_[0] && $_[0]->isa($class);
      66        
27              
28 241         331 my $data;
29 241 100       669 $data = shift if is_plain_hashref($_[0]);
30              
31 241         307 my $kdbx;
32 241 100       524 $kdbx = shift if @_ % 2 == 1;
33              
34 241         658 my %args = @_;
35 241 100 33     772 $args{kdbx} //= $kdbx if defined $kdbx;
36              
37 241   100     756 my $self = bless $data // {}, $class;
38 241         975 $self->init(%args);
39 241 100       836 $self->_set_nonlazy_attributes if !$data;
40 241         6112 return $self;
41             }
42              
43 0     0   0 sub _set_nonlazy_attributes { die 'Not implemented' }
44              
45              
46             sub init {
47 136     136 1 181 my $self = shift;
48 136         316 my %args = @_;
49              
50 136         406 while (my ($key, $val) = each %args) {
51 343 50       1268 if (my $method = $self->can($key)) {
52 343         704 $self->$method($val);
53             }
54             }
55              
56 136         347 return $self;
57             }
58              
59              
60             sub wrap {
61 813     813 1 1116 my $class = shift;
62 813         946 my $object = shift;
63 813 100 66     4875 return $object if blessed $object && $object->isa($class);
64 115 100       305 return $class->new(@_, @$object) if is_arrayref($object);
65 72         204 return $class->new($object, @_);
66             }
67              
68              
69 0     0 1 0 sub label { die 'Not implemented' }
70              
71              
72             my %CLONE = (entries => 1, groups => 1, history => 1);
73             sub clone {
74 31     31 1 85 my $self = shift;
75 31         83 my %args = @_;
76              
77 31   100     153 local $CLONE{new_uuid} = $args{new_uuid} // $args{parent} // 0;
      100        
78 31   100     103 local $CLONE{entries} = $args{entries} // 1;
79 31   100     79 local $CLONE{groups} = $args{groups} // 1;
80 31   100     121 local $CLONE{history} = $args{history} // 1;
81 31   50     78 local $CLONE{reference_password} = $args{reference_password} // 0;
82 31   100     73 local $CLONE{reference_username} = $args{reference_username} // 0;
83              
84 31         122 require Storable;
85 31         409 my $copy = Storable::dclone($self);
86              
87 31 100 66     90 if ($args{relabel} and my $label = $self->label) {
88 3         13 $copy->label("$label - Copy");
89             }
90 31 100 66     71 if ($args{parent} and my $parent = $self->group) {
91 3         11 $parent->add_object($copy);
92             }
93              
94 31         333 return $copy;
95             }
96              
97             sub STORABLE_freeze {
98 64     64 0 110 my $self = shift;
99 64         84 my $cloning = shift;
100              
101 64         548 my $copy = {%$self};
102 64 100       184 delete $copy->{entries} if !$CLONE{entries};
103 64 100       108 delete $copy->{groups} if !$CLONE{groups};
104 64 100       90 delete $copy->{history} if !$CLONE{history};
105              
106 64 50       4261 return ($cloning ? Hash::Util::FieldHash::id($self) : ''), $copy;
107             }
108              
109             sub STORABLE_thaw {
110 64     64 0 143 my $self = shift;
111 64         71 my $cloning = shift;
112 64         75 my $addr = shift;
113 64         68 my $copy = shift;
114              
115 64         567 @$self{keys %$copy} = values %$copy;
116              
117 64 50       163 if ($cloning) {
118 64         131 my $kdbx = $KDBX{$addr};
119 64 100       166 $self->kdbx($kdbx) if $kdbx;
120             }
121              
122 64 50       139 if (defined $self->{uuid}) {
123 64 100 66     236 if (($CLONE{reference_password} || $CLONE{reference_username}) && $self->can('strings')) {
      66        
124 2         9 my $uuid = format_uuid($self->{uuid});
125 2         3 my $clone_obj = do {
126 2         6 local $CLONE{new_uuid} = 0;
127 2         5 local $CLONE{entries} = 1;
128 2         3 local $CLONE{groups} = 1;
129 2         4 local $CLONE{history} = 1;
130 2         5 local $CLONE{reference_password} = 0;
131 2         3 local $CLONE{reference_username} = 0;
132             # Clone only the entry's data and manually bless to avoid infinite recursion.
133 2         110 bless Storable::dclone({%$copy}), 'File::KDBX::Entry';
134             };
135 2         8 my $txn = $self->begin_work(snapshot => $clone_obj);
136 2 50       6 if ($CLONE{reference_password}) {
137 0         0 $self->password("{REF:P\@I:$uuid}");
138             }
139 2 50       6 if ($CLONE{reference_username}) {
140 2         9 $self->username("{REF:U\@I:$uuid}");
141             }
142 2         6 $txn->commit;
143             }
144 64 100       145 $self->uuid(generate_uuid) if $CLONE{new_uuid};
145             }
146              
147             # Dualvars aren't cloned as dualvars, so dualify the icon.
148 64 50       215 $self->icon_id($self->{icon_id}) if defined $self->{icon_id};
149             }
150              
151              
152             sub kdbx {
153 1066     1066 1 5207 my $self = shift;
154 1066 50       2209 $self = $self->new if !ref $self;
155 1066 100       1753 if (@_) {
156 315 50       559 if (my $kdbx = shift) {
157 315         1654 $KDBX{$self} = $kdbx;
158 315         904 weaken $KDBX{$self};
159             }
160             else {
161 0         0 delete $KDBX{$self};
162             }
163             }
164 1066 100       4103 $KDBX{$self} or throw 'Object is disconnected', object => $self;
165             }
166              
167              
168             sub is_connected {
169 85     85 1 148 my $self = shift;
170 85         151 return !!eval { $self->kdbx };
  85         321  
171             }
172              
173              
174 5     5 1 47 sub id { format_uuid(shift->uuid, @_) }
175              
176              
177             sub group {
178 157     157 1 200 my $self = shift;
179              
180 157 100       287 if (my $new_group = shift) {
181 1         5 my $old_group = $self->group;
182 1 50       7 return $new_group if Hash::Util::FieldHash::id($old_group) == Hash::Util::FieldHash::id($new_group);
183             # move to a new parent
184 1 50       5 $self->remove(signal => 0) if $old_group;
185 1         5 $self->location_changed('now');
186 1         18 $new_group->add_object($self);
187             }
188              
189 157         312 my $id = Hash::Util::FieldHash::id($self);
190 157 100       340 if (my $group = $PARENT{$self}) {
191 31         67 my $method = $self->_parent_container;
192 31 50   31   83 return $group if first { $id == Hash::Util::FieldHash::id($_) } @{$group->$method};
  31         148  
  31         85  
193 0         0 delete $PARENT{$self};
194             }
195             # always get lineage from root to leaf because the other way requires parent, so it would be recursive
196 126 100       210 my $lineage = $self->kdbx->_trace_lineage($self) or return;
197 1 50       5 my $group = pop @$lineage or return;
198 1         3 $PARENT{$self} = $group; weaken $PARENT{$self};
  1         4  
199 1         3 return $group;
200             }
201              
202             sub _set_group {
203 112     112   150 my $self = shift;
204 112 100       189 if (my $parent = shift) {
205 49         138 $PARENT{$self} = $parent;
206 49         134 weaken $PARENT{$self};
207             }
208             else {
209 63         165 delete $PARENT{$self};
210             }
211 112         229 return $self;
212             }
213              
214             ### Name of the parent attribute expected to contain the object
215 0     0   0 sub _parent_container { die 'Not implemented' }
216              
217              
218             sub lineage {
219 5     5 1 12 my $self = shift;
220 5         9 my $base = shift;
221              
222 5 50       11 my $base_addr = $base ? Hash::Util::FieldHash::id($base) : 0;
223              
224             # try leaf to root
225 5         11 my @path;
226 5         8 my $object = $self;
227 5         13 while ($object = $object->group) {
228 8         17 unshift @path, $object;
229 8 50       23 last if $base_addr == Hash::Util::FieldHash::id($object);
230             }
231 5 50 33     37 return \@path if @path && ($base_addr == Hash::Util::FieldHash::id($path[0]) || $path[0]->is_root);
      33        
232              
233             # try root to leaf
234 0         0 return $self->kdbx->_trace_lineage($self, $base);
235             }
236              
237              
238             sub remove {
239 56     56 1 75 my $self = shift;
240 56         133 my $parent = $self->group;
241 56 100       152 $parent->remove_object($self, @_) if $parent;
242 56         192 $self->_set_group(undef);
243 56         161 return $self;
244             }
245              
246              
247             sub recycle {
248 1     1 1 3 my $self = shift;
249 1         3 return $self->group($self->kdbx->recycle_bin);
250             }
251              
252              
253             sub recycle_or_remove {
254 3     3 1 24 my $self = shift;
255 3         5 my $kdbx = eval { $self->kdbx };
  3         7  
256 3 100 66     12 if ($kdbx && $kdbx->recycle_bin_enabled && !$self->is_recycled) {
      100        
257 1         7 $self->recycle;
258             }
259             else {
260 2         24 $self->remove;
261             }
262             }
263              
264              
265             sub is_recycled {
266 4     4 1 40 my $self = shift;
267 4 50       6 eval { $self->kdbx } or return FALSE;
  4         9  
268 4   100 5   9 return !!($self->group && any { $_->is_recycle_bin } @{$self->lineage});
  5         14  
269             }
270              
271             ##############################################################################
272              
273              
274             sub tag_list {
275 0     0 1 0 my $self = shift;
276 0   0     0 return grep { $_ ne '' } split(/[,\.:;]|\s+/, trim($self->tags) // '');
  0         0  
277             }
278              
279              
280             sub custom_icon {
281 7     7 1 22 my $self = shift;
282 7         16 my $kdbx = $self->kdbx;
283 7 100       17 if (@_) {
284 4         8 my $img = shift;
285 4 100       17 my $uuid = defined $img ? $kdbx->add_custom_icon($img, @_) : undef;
286 4 100       18 $self->icon_id(0) if $uuid;
287 4         14 $self->custom_icon_uuid($uuid);
288 4         13 return $img;
289             }
290 3         9 return $kdbx->custom_icon_data($self->custom_icon_uuid);
291             }
292              
293              
294             sub custom_data {
295 526     526 1 857 my $self = shift;
296 526 50 66     1083 $self->{custom_data} = shift if @_ == 1 && is_plain_hashref($_[0]);
297 526 100 100     2570 return $self->{custom_data} //= {} if !@_;
298              
299 19 100       78 my %args = @_ == 2 ? (key => shift, value => shift)
    100          
300             : @_ % 2 == 1 ? (key => shift, @_) : @_;
301              
302 19 50 66     48 if (!$args{key} && !$args{value}) {
303 2         6 my %standard = (key => 1, value => 1, last_modification_time => 1);
304 2         5 my @other_keys = grep { !$standard{$_} } keys %args;
  4         11  
305 2 50       7 if (@other_keys == 1) {
306 2         5 my $key = $args{key} = $other_keys[0];
307 2         5 $args{value} = delete $args{$key};
308             }
309             }
310              
311 19 50       39 my $key = $args{key} or throw 'Must provide a custom_data key to access';
312              
313 19 50       53 return $self->{custom_data}{$key} = $args{value} if is_plain_hashref($args{value});
314              
315 19         61 while (my ($field, $value) = each %args) {
316 34         110 $self->{custom_data}{$key}{$field} = $value;
317             }
318 19         51 return $self->{custom_data}{$key};
319             }
320              
321              
322             sub custom_data_value {
323 6     6 1 53 my $self = shift;
324 6   50     16 my $data = $self->custom_data(@_) // return undef;
325 6         33 return $data->{value};
326             }
327              
328             ##############################################################################
329              
330              
331             sub begin_work {
332 35     35 1 1644 my $self = shift;
333              
334 35 100       77 if (defined wantarray) {
335 16         874 require File::KDBX::Transaction;
336 16         59 return File::KDBX::Transaction->new($self, @_);
337             }
338              
339 19         33 my %args = @_;
340 19   66     55 my $orig = $args{snapshot} // do {
341             my $c = $self->clone(
342             entries => $args{entries} // 0,
343             groups => $args{groups} // 0,
344 17   100     125 history => $args{history} // 0,
      50        
      50        
345             );
346 17 100       50 $c->{entries} = $self->{entries} if !$args{entries};
347 17 50       60 $c->{groups} = $self->{groups} if !$args{groups};
348 17 50       44 $c->{history} = $self->{history} if !$args{history};
349 17         43 $c;
350             };
351              
352 19         46 my $id = Hash::Util::FieldHash::id($orig);
353 19         50 _save_references($id, $self, $orig);
354              
355 19         59 $self->_signal_begin_work;
356              
357 19         27 push @{$self->_txns}, $orig;
  19         40  
358             }
359              
360              
361             sub commit {
362 14     14 1 20 my $self = shift;
363 14 50       20 my $orig = pop @{$self->_txns} or return $self;
  14         24  
364 14         52 $self->_commit($orig);
365 14         184 my $signals = $self->_signal_commit;
366 14 100       31 $self->_signal_send($signals) if !$self->_in_txn;
367 14         87 return $self;
368             }
369              
370              
371             sub rollback {
372 5     5 1 9 my $self = shift;
373              
374 5 50       5 my $orig = pop @{$self->_txns} or return $self;
  5         10  
375              
376 5         13 my $id = Hash::Util::FieldHash::id($orig);
377 5         11 _restore_references($id, $orig);
378              
379 5         17 $self->_signal_rollback;
380              
381 5         44 return $self;
382             }
383              
384             # Get whether or not there is at least one pending transaction.
385 83     83   101 sub _in_txn { scalar @{$_[0]->_txns} }
  83         164  
386              
387             # Get an array ref of pending transactions.
388 153   100 153   913 sub _txns { $TXNS{$_[0]} //= [] }
389              
390             # The _commit hook notifies subclasses that a commit has occurred.
391 0     0   0 sub _commit { die 'Not implemented' }
392              
393             # Get a reference to an object that represents an object's committed state. If there is no pending
394             # transaction, this is just $self. If there is a transaction, this is the snapshot taken immediately before
395             # the transaction began. This method is private because it provides direct access to the actual snapshot. It
396             # is important that the snapshot not be changed or a rollback would roll back to an altered state.
397             # This is used by File::KDBX::Dumper::XML so as to not dump uncommitted changes.
398             sub _committed {
399 32     32   59 my $self = shift;
400 32         51 my ($orig) = @{$self->_txns};
  32         89  
401 32   66     192 return $orig // $self;
402             }
403              
404             # In addition to cloning an object when beginning work, we also keep track its hashrefs and arrayrefs
405             # internally so that we can restore to the very same structures in the case of a rollback.
406             sub _save_references {
407 1406     1406   1552 my $id = shift;
408 1406         1463 my $self = shift;
409 1406         1392 my $orig = shift;
410              
411 1406 100 100     4248 if (is_plain_arrayref($orig)) {
    100 66        
412 67         135 for (my $i = 0; $i < @$orig; ++$i) {
413 20         35 _save_references($id, $self->[$i], $orig->[$i]);
414             }
415 67         199 $REFS{$id}{Hash::Util::FieldHash::id($orig)} = $self;
416             }
417             elsif (is_plain_hashref($orig) || (blessed $orig && $orig->isa(__PACKAGE__))) {
418 349         692 for my $key (keys %$orig) {
419 1367         1951 _save_references($id, $self->{$key}, $orig->{$key});
420             }
421 349         1095 $REFS{$id}{Hash::Util::FieldHash::id($orig)} = $self;
422             }
423             }
424              
425             # During a rollback, copy data from the snapshot back into the original internal structures.
426             sub _restore_references {
427 377     377   432 my $id = shift;
428 377   100     583 my $orig = shift // return;
429 329   50     1641 my $self = delete $REFS{$id}{Hash::Util::FieldHash::id($orig) // ''} // return $orig;
      100        
430              
431 108 100 33     268 if (is_plain_arrayref($orig)) {
    50 66        
432 19         34 @$self = map { _restore_references($id, $_) } @$orig;
  6         10  
433             }
434             elsif (is_plain_hashref($orig) || (blessed $orig && $orig->isa(__PACKAGE__))) {
435 89         175 for my $key (keys %$orig) {
436             # next if is_ref($orig->{$key}) &&
437             # (Hash::Util::FieldHash::id($self->{$key}) // 0) == Hash::Util::FieldHash::id($orig->{$key});
438 366         486 $self->{$key} = _restore_references($id, $orig->{$key});
439             }
440             }
441              
442 108         201 return $self;
443             }
444              
445             ##############################################################################
446              
447             sub _signal {
448 69     69   95 my $self = shift;
449 69         90 my $type = shift;
450              
451 69 50       157 if ($self->_in_txn) {
452 0         0 my $stack = $self->_signal_stack;
453 0         0 my $queue = $stack->[-1];
454 0         0 push @$queue, [$type, @_];
455             }
456              
457 69         345 $self->_signal_send([[$type, @_]]);
458              
459 69         311 return $self;
460             }
461              
462 52   100 52   219 sub _signal_stack { $SIGNALS{$_[0]} //= [] }
463              
464             sub _signal_begin_work {
465 19     19   29 my $self = shift;
466 19         24 push @{$self->_signal_stack}, [];
  19         43  
467             }
468              
469             sub _signal_commit {
470 14     14   22 my $self = shift;
471 14         19 my $signals = pop @{$self->_signal_stack};
  14         30  
472 14   100     27 my $previous = $self->_signal_stack->[-1] // [];
473 14         32 push @$previous, @$signals;
474 14         22 return $previous;
475             }
476              
477             sub _signal_rollback {
478 5     5   7 my $self = shift;
479 5         6 pop @{$self->_signal_stack};
  5         10  
480             }
481              
482             sub _signal_send {
483 81     81   108 my $self = shift;
484 81   50     154 my $signals = shift // [];
485              
486 81 50       219 my $kdbx = $KDBX{$self} or return;
487              
488             # de-duplicate, keeping the most recent signal for each type
489 81         104 my %seen;
490 81         155 my @signals = grep { !$seen{$_->[0]}++ } reverse @$signals;
  69         280  
491              
492 81         161 for my $sig (reverse @signals) {
493 69         217 $kdbx->_handle_signal($self, @$sig);
494             }
495             }
496              
497             ##############################################################################
498              
499             sub _wrap_group {
500 54     54   76 my $self = shift;
501 54         69 my $group = shift;
502 54         166 require File::KDBX::Group;
503 54         148 return File::KDBX::Group->wrap($group, $KDBX{$self});
504             }
505              
506             sub _wrap_entry {
507 60     60   99 my $self = shift;
508 60         80 my $entry = shift;
509 60         3438 require File::KDBX::Entry;
510 60         287 return File::KDBX::Entry->wrap($entry, $KDBX{$self});
511             }
512              
513 0     0 0   sub TO_JSON { +{%{$_[0]}} }
  0            
514              
515             1;
516              
517             __END__