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   6719 use warnings;
  11         23  
  11         356  
5 11     11   51 use strict;
  11         19  
  11         214  
6              
7 11     11   46 use Devel::GlobalDestruction;
  11         16  
  11         61  
8 11     11   709 use File::KDBX::Constants qw(:bool);
  11         17  
  11         1035  
9 11     11   60 use File::KDBX::Error;
  11         21  
  11         416  
10 11     11   60 use File::KDBX::Util qw(:uuid);
  11         20  
  11         985  
11 11     11   60 use Hash::Util::FieldHash qw(fieldhashes);
  11         21  
  11         440  
12 11     11   56 use List::Util qw(any first);
  11         18  
  11         600  
13 11     11   56 use Ref::Util qw(is_arrayref is_plain_arrayref is_plain_hashref is_ref);
  11         17  
  11         507  
14 11     11   63 use Scalar::Util qw(blessed weaken);
  11         22  
  11         448  
15 11     11   62 use namespace::clean;
  11         18  
  11         56  
16              
17             our $VERSION = '0.905'; # VERSION
18              
19             fieldhashes \my (%KDBX, %PARENT, %TXNS, %REFS, %SIGNALS);
20              
21              
22             sub new {
23 237     237 1 99224 my $class = shift;
24              
25             # copy constructor
26 237 50 100     705 return $_[0]->clone if @_ == 1 && blessed $_[0] && $_[0]->isa($class);
      66        
27              
28 237         319 my $data;
29 237 100       680 $data = shift if is_plain_hashref($_[0]);
30              
31 237         301 my $kdbx;
32 237 100       526 $kdbx = shift if @_ % 2 == 1;
33              
34 237         573 my %args = @_;
35 237 100 33     757 $args{kdbx} //= $kdbx if defined $kdbx;
36              
37 237   100     784 my $self = bless $data // {}, $class;
38 237         891 $self->init(%args);
39 237 100       754 $self->_set_nonlazy_attributes if !$data;
40 237         5608 return $self;
41             }
42              
43 0     0   0 sub _set_nonlazy_attributes { die 'Not implemented' }
44              
45              
46             sub init {
47 133     133 1 184 my $self = shift;
48 133         311 my %args = @_;
49              
50 133         382 while (my ($key, $val) = each %args) {
51 331 50       1156 if (my $method = $self->can($key)) {
52 331         652 $self->$method($val);
53             }
54             }
55              
56 133         309 return $self;
57             }
58              
59              
60             sub wrap {
61 798     798 1 1078 my $class = shift;
62 798         888 my $object = shift;
63 798 100 66     4817 return $object if blessed $object && $object->isa($class);
64 114 100       293 return $class->new(@_, @$object) if is_arrayref($object);
65 72         188 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 54 my $self = shift;
75 31         79 my %args = @_;
76              
77 31   100     141 local $CLONE{new_uuid} = $args{new_uuid} // $args{parent} // 0;
      100        
78 31   100     101 local $CLONE{entries} = $args{entries} // 1;
79 31   100     72 local $CLONE{groups} = $args{groups} // 1;
80 31   100     61 local $CLONE{history} = $args{history} // 1;
81 31   50     81 local $CLONE{reference_password} = $args{reference_password} // 0;
82 31   100     84 local $CLONE{reference_username} = $args{reference_username} // 0;
83              
84 31         122 require Storable;
85 31         437 my $copy = Storable::dclone($self);
86              
87 31 100 66     87 if ($args{relabel} and my $label = $self->label) {
88 3         11 $copy->label("$label - Copy");
89             }
90 31 100 66     69 if ($args{parent} and my $parent = $self->group) {
91 3         8 $parent->add_object($copy);
92             }
93              
94 31         304 return $copy;
95             }
96              
97             sub STORABLE_freeze {
98 64     64 0 113 my $self = shift;
99 64         73 my $cloning = shift;
100              
101 64         565 my $copy = {%$self};
102 64 100       172 delete $copy->{entries} if !$CLONE{entries};
103 64 100       107 delete $copy->{groups} if !$CLONE{groups};
104 64 100       110 delete $copy->{history} if !$CLONE{history};
105              
106 64 50       3959 return ($cloning ? Hash::Util::FieldHash::id($self) : ''), $copy;
107             }
108              
109             sub STORABLE_thaw {
110 64     64 0 133 my $self = shift;
111 64         79 my $cloning = shift;
112 64         75 my $addr = shift;
113 64         73 my $copy = shift;
114              
115 64         479 @$self{keys %$copy} = values %$copy;
116              
117 64 50       180 if ($cloning) {
118 64         121 my $kdbx = $KDBX{$addr};
119 64 100       142 $self->kdbx($kdbx) if $kdbx;
120             }
121              
122 64 50       125 if (defined $self->{uuid}) {
123 64 100 66     226 if (($CLONE{reference_password} || $CLONE{reference_username}) && $self->can('strings')) {
      66        
124 2         6 my $uuid = format_uuid($self->{uuid});
125 2         3 my $clone_obj = do {
126 2         5 local $CLONE{new_uuid} = 0;
127 2         3 local $CLONE{entries} = 1;
128 2         3 local $CLONE{groups} = 1;
129 2         5 local $CLONE{history} = 1;
130 2         4 local $CLONE{reference_password} = 0;
131 2         2 local $CLONE{reference_username} = 0;
132             # Clone only the entry's data and manually bless to avoid infinite recursion.
133 2         107 bless Storable::dclone({%$copy}), 'File::KDBX::Entry';
134             };
135 2         8 my $txn = $self->begin_work(snapshot => $clone_obj);
136 2 50       7 if ($CLONE{reference_password}) {
137 0         0 $self->password("{REF:P\@I:$uuid}");
138             }
139 2 50       5 if ($CLONE{reference_username}) {
140 2         8 $self->username("{REF:U\@I:$uuid}");
141             }
142 2         5 $txn->commit;
143             }
144 64 100       139 $self->uuid(generate_uuid) if $CLONE{new_uuid};
145             }
146              
147             # Dualvars aren't cloned as dualvars, so dualify the icon.
148 64 50       199 $self->icon_id($self->{icon_id}) if defined $self->{icon_id};
149             }
150              
151              
152             sub kdbx {
153 978     978 1 4826 my $self = shift;
154 978 50       1696 $self = $self->new if !ref $self;
155 978 100       1601 if (@_) {
156 310 50       506 if (my $kdbx = shift) {
157 310         1632 $KDBX{$self} = $kdbx;
158 310         819 weaken $KDBX{$self};
159             }
160             else {
161 0         0 delete $KDBX{$self};
162             }
163             }
164 978 100       3421 $KDBX{$self} or throw 'Object is disconnected', object => $self;
165             }
166              
167              
168             sub is_connected {
169 84     84 1 133 my $self = shift;
170 84         126 return !!eval { $self->kdbx };
  84         159  
171             }
172              
173              
174 5     5 1 44 sub id { format_uuid(shift->uuid, @_) }
175              
176              
177             sub group {
178 154     154 1 191 my $self = shift;
179              
180 154 100       293 if (my $new_group = shift) {
181 1         5 my $old_group = $self->group;
182 1 50       6 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       4 $self->remove(signal => 0) if $old_group;
185 1         4 $self->location_changed('now');
186 1         17 $new_group->add_object($self);
187             }
188              
189 154         275 my $id = Hash::Util::FieldHash::id($self);
190 154 100       316 if (my $group = $PARENT{$self}) {
191 31         64 my $method = $self->_parent_container;
192 31 50   31   81 return $group if first { $id == Hash::Util::FieldHash::id($_) } @{$group->$method};
  31         150  
  31         76  
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 123 100       207 my $lineage = $self->kdbx->_trace_lineage($self) or return;
197 1 50       6 my $group = pop @$lineage or return;
198 1         4 $PARENT{$self} = $group; weaken $PARENT{$self};
  1         3  
199 1         2 return $group;
200             }
201              
202             sub _set_group {
203 110     110   135 my $self = shift;
204 110 100       173 if (my $parent = shift) {
205 48         121 $PARENT{$self} = $parent;
206 48         116 weaken $PARENT{$self};
207             }
208             else {
209 62         171 delete $PARENT{$self};
210             }
211 110         211 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 8 my $self = shift;
220 5         6 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         6 my @path;
226 5         7 my $object = $self;
227 5         10 while ($object = $object->group) {
228 8         13 unshift @path, $object;
229 8 50       22 last if $base_addr == Hash::Util::FieldHash::id($object);
230             }
231 5 50 33     29 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 55     55 1 88 my $self = shift;
240 55         104 my $parent = $self->group;
241 55 100       122 $parent->remove_object($self, @_) if $parent;
242 55         163 $self->_set_group(undef);
243 55         121 return $self;
244             }
245              
246              
247             sub recycle {
248 1     1 1 2 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 19 my $self = shift;
255 3         5 my $kdbx = eval { $self->kdbx };
  3         6  
256 3 100 66     13 if ($kdbx && $kdbx->recycle_bin_enabled && !$self->is_recycled) {
      100        
257 1         5 $self->recycle;
258             }
259             else {
260 2         21 $self->remove;
261             }
262             }
263              
264              
265             sub is_recycled {
266 4     4 1 39 my $self = shift;
267 4 50       5 eval { $self->kdbx } or return FALSE;
  4         7  
268 4   100 5   9 return !!($self->group && any { $_->is_recycle_bin } @{$self->lineage});
  5         12  
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 25 my $self = shift;
282 7         14 my $kdbx = $self->kdbx;
283 7 100       19 if (@_) {
284 4         6 my $img = shift;
285 4 100       15 my $uuid = defined $img ? $kdbx->add_custom_icon($img, @_) : undef;
286 4 100       24 $self->icon_id(0) if $uuid;
287 4         11 $self->custom_icon_uuid($uuid);
288 4         14 return $img;
289             }
290 3         8 return $kdbx->custom_icon_data($self->custom_icon_uuid);
291             }
292              
293              
294             sub custom_data {
295 517     517 1 713 my $self = shift;
296 517 50 66     1151 $self->{custom_data} = shift if @_ == 1 && is_plain_hashref($_[0]);
297 517 100 100     2380 return $self->{custom_data} //= {} if !@_;
298              
299 19 100       70 my %args = @_ == 2 ? (key => shift, value => shift)
    100          
300             : @_ % 2 == 1 ? (key => shift, @_) : @_;
301              
302 19 50 66     40 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         9  
305 2 50       6 if (@other_keys == 1) {
306 2         4 my $key = $args{key} = $other_keys[0];
307 2         5 $args{value} = delete $args{$key};
308             }
309             }
310              
311 19 50       36 my $key = $args{key} or throw 'Must provide a custom_data key to access';
312              
313 19 50       46 return $self->{custom_data}{$key} = $args{value} if is_plain_hashref($args{value});
314              
315 19         53 while (my ($field, $value) = each %args) {
316 34         103 $self->{custom_data}{$key}{$field} = $value;
317             }
318 19         44 return $self->{custom_data}{$key};
319             }
320              
321              
322             sub custom_data_value {
323 6     6 1 51 my $self = shift;
324 6   50     13 my $data = $self->custom_data(@_) // return undef;
325 6         47 return $data->{value};
326             }
327              
328             ##############################################################################
329              
330              
331             sub begin_work {
332 35     35 1 1609 my $self = shift;
333              
334 35 100       74 if (defined wantarray) {
335 16         807 require File::KDBX::Transaction;
336 16         56 return File::KDBX::Transaction->new($self, @_);
337             }
338              
339 19         34 my %args = @_;
340 19   66     49 my $orig = $args{snapshot} // do {
341             my $c = $self->clone(
342             entries => $args{entries} // 0,
343             groups => $args{groups} // 0,
344 17   100     157 history => $args{history} // 0,
      50        
      50        
345             );
346 17 100       51 $c->{entries} = $self->{entries} if !$args{entries};
347 17 50       59 $c->{groups} = $self->{groups} if !$args{groups};
348 17 50       40 $c->{history} = $self->{history} if !$args{history};
349 17         41 $c;
350             };
351              
352 19         44 my $id = Hash::Util::FieldHash::id($orig);
353 19         47 _save_references($id, $self, $orig);
354              
355 19         64 $self->_signal_begin_work;
356              
357 19         30 push @{$self->_txns}, $orig;
  19         30  
358             }
359              
360              
361             sub commit {
362 14     14 1 24 my $self = shift;
363 14 50       19 my $orig = pop @{$self->_txns} or return $self;
  14         24  
364 14         50 $self->_commit($orig);
365 14         191 my $signals = $self->_signal_commit;
366 14 100       32 $self->_signal_send($signals) if !$self->_in_txn;
367 14         83 return $self;
368             }
369              
370              
371             sub rollback {
372 5     5 1 7 my $self = shift;
373              
374 5 50       8 my $orig = pop @{$self->_txns} or return $self;
  5         11  
375              
376 5         12 my $id = Hash::Util::FieldHash::id($orig);
377 5         12 _restore_references($id, $orig);
378              
379 5         17 $self->_signal_rollback;
380              
381 5         45 return $self;
382             }
383              
384             # Get whether or not there is at least one pending transaction.
385 82     82   96 sub _in_txn { scalar @{$_[0]->_txns} }
  82         163  
386              
387             # Get an array ref of pending transactions.
388 152   100 152   831 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 take before the transaction
395             # began. This method is private because it provides direct access to the actual snapshot. It is important that
396             # 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   57 my $self = shift;
400 32         35 my ($orig) = @{$self->_txns};
  32         76  
401 32   66     195 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   1542 my $id = shift;
408 1406         1448 my $self = shift;
409 1406         1406 my $orig = shift;
410              
411 1406 100 100     4118 if (is_plain_arrayref($orig)) {
    100 66        
412 67         132 for (my $i = 0; $i < @$orig; ++$i) {
413 20         38 _save_references($id, $self->[$i], $orig->[$i]);
414             }
415 67         229 $REFS{$id}{Hash::Util::FieldHash::id($orig)} = $self;
416             }
417             elsif (is_plain_hashref($orig) || (blessed $orig && $orig->isa(__PACKAGE__))) {
418 349         640 for my $key (keys %$orig) {
419 1367         1928 _save_references($id, $self->{$key}, $orig->{$key});
420             }
421 349         1039 $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   388 my $id = shift;
428 377   100     590 my $orig = shift // return;
429 329   50     1643 my $self = delete $REFS{$id}{Hash::Util::FieldHash::id($orig) // ''} // return $orig;
      100        
430              
431 108 100 33     262 if (is_plain_arrayref($orig)) {
    50 66        
432 19         36 @$self = map { _restore_references($id, $_) } @$orig;
  6         10  
433             }
434             elsif (is_plain_hashref($orig) || (blessed $orig && $orig->isa(__PACKAGE__))) {
435 89         167 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         495 $self->{$key} = _restore_references($id, $orig->{$key});
439             }
440             }
441              
442 108         189 return $self;
443             }
444              
445             ##############################################################################
446              
447             sub _signal {
448 68     68   91 my $self = shift;
449 68         90 my $type = shift;
450              
451 68 50       151 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 68         265 $self->_signal_send([[$type, @_]]);
458              
459 68         278 return $self;
460             }
461              
462 52   100 52   226 sub _signal_stack { $SIGNALS{$_[0]} //= [] }
463              
464             sub _signal_begin_work {
465 19     19   25 my $self = shift;
466 19         25 push @{$self->_signal_stack}, [];
  19         43  
467             }
468              
469             sub _signal_commit {
470 14     14   23 my $self = shift;
471 14         25 my $signals = pop @{$self->_signal_stack};
  14         31  
472 14   100     28 my $previous = $self->_signal_stack->[-1] // [];
473 14         30 push @$previous, @$signals;
474 14         22 return $previous;
475             }
476              
477             sub _signal_rollback {
478 5     5   8 my $self = shift;
479 5         5 pop @{$self->_signal_stack};
  5         11  
480             }
481              
482             sub _signal_send {
483 80     80   128 my $self = shift;
484 80   50     156 my $signals = shift // [];
485              
486 80 50       199 my $kdbx = $KDBX{$self} or return;
487              
488             # de-duplicate, keeping the most recent signal for each type
489 80         103 my %seen;
490 80         149 my @signals = grep { !$seen{$_->[0]}++ } reverse @$signals;
  68         263  
491              
492 80         157 for my $sig (reverse @signals) {
493 68         185 $kdbx->_handle_signal($self, @$sig);
494             }
495             }
496              
497             ##############################################################################
498              
499             sub _wrap_group {
500 54     54   71 my $self = shift;
501 54         69 my $group = shift;
502 54         166 require File::KDBX::Group;
503 54         135 return File::KDBX::Group->wrap($group, $KDBX{$self});
504             }
505              
506             sub _wrap_entry {
507 59     59   88 my $self = shift;
508 59         117 my $entry = shift;
509 59         3299 require File::KDBX::Entry;
510 59         264 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__