File Coverage

blib/lib/File/KDBX/Group.pm
Criterion Covered Total %
statement 275 311 88.4
branch 118 178 66.2
condition 78 122 63.9
subroutine 61 70 87.1
pod 43 44 97.7
total 575 725 79.3


line stmt bran cond sub pod time code
1             package File::KDBX::Group;
2             # ABSTRACT: A KDBX database group
3              
4 9     9   90593 use warnings;
  9         14  
  9         264  
5 9     9   41 use strict;
  9         14  
  9         166  
6              
7 9     9   368 use Devel::GlobalDestruction;
  9         463  
  9         61  
8 9     9   542 use File::KDBX::Constants qw(:bool :icon :iteration);
  9         22  
  9         3228  
9 9     9   63 use File::KDBX::Error;
  9         15  
  9         391  
10 9     9   3623 use File::KDBX::Iterator;
  9         21  
  9         283  
11 9     9   54 use File::KDBX::Util qw(:assert :class :coercion generate_uuid);
  9         17  
  9         1205  
12 9     9   726 use Hash::Util::FieldHash;
  9         2494  
  9         338  
13 9     9   49 use List::Util qw(any sum0);
  9         13  
  9         484  
14 9     9   42 use Ref::Util qw(is_coderef is_ref);
  9         14  
  9         287  
15 9     9   38 use Scalar::Util qw(blessed);
  9         24  
  9         325  
16 9     9   43 use Time::Piece 1.33;
  9         210  
  9         68  
17 9     9   727 use boolean;
  9         15  
  9         58  
18 9     9   520 use namespace::clean;
  9         14  
  9         47  
19              
20             extends 'File::KDBX::Object';
21              
22             our $VERSION = '0.906'; # VERSION
23 236 50   236 1 686  
24 236 100   155 1 663  
  155 50       435  
25 236 100 66 243 1 879 # has uuid => sub { generate_uuid(printable => 1) };
  155 50       437  
  243         586  
26 155 100 66 106 1 560 has name => '', coerce => \&to_string;
  243 50       492  
  106         650  
27 243 100 66 91 1 1233 has notes => '', coerce => \&to_string;
  106 50       252  
  91         253  
28 106 50 66 154 1 597 has tags => '', coerce => \&to_string;
  91 50       194  
  154         412  
29 91 100 50 87 1 367 has icon_id => ICON_FOLDER, coerce => \&to_icon_constant;
  154 50       417  
  87         650  
30 154 50 66 88 1 1002 has custom_icon_uuid => undef, coerce => \&to_uuid;
  87 50       275  
  88         234  
31 87 100 66 89 1 470 has is_expanded => false, coerce => \&to_bool;
  88 50       191  
  89         834  
32 88 100 100 83 1 401 has default_auto_type_sequence => '', coerce => \&to_string;
  89 50       191  
  83         233  
33 89 50 100     423 has enable_auto_type => undef, coerce => \&to_tristate;
  83         158  
34 83 50 50 244 1 350 has enable_searching => undef, coerce => \&to_tristate;
  244         639  
35 244 100       463 has last_top_visible_entry => undef, coerce => \&to_uuid;
36 244   100     1043 # has custom_data => {};
37 692 50   692 0 1359 has previous_parent_group => undef, coerce => \&to_uuid;
38 692 50       1100 # has entries => [];
39 692 50 100 89 1 3519 # has groups => [];
  89         280  
40 89 100   83 1 218 has times => {};
  83 50       6229  
41 89 50 100 89 1 181  
  83 50       208  
  89         4996  
42 83 100 50 83 1 195 has last_modification_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  89 50       266  
  83         4809  
43 89 50 100 87 1 218 has creation_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  83 50       214  
  87         4804  
44 83 50 50 83 1 359 has last_access_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  87 50       194  
  83         590  
45 87 50 66 83 1 180 has expiry_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  83 50       187  
  83         357  
46 83 50 33     160 has expires => false, store => 'times', coerce => \&to_bool;
  83         193  
47 83   50     153 has usage_count => 0, store => 'times', coerce => \&to_number;
48             has location_changed => sub { gmtime }, store => 'times', coerce => \&to_time;
49              
50             my @ATTRS = qw(uuid custom_data entries groups);
51             sub _set_nonlazy_attributes {
52 83     83   120 my $self = shift;
53 83         247 $self->$_ for @ATTRS, list_attributes(ref $self);
54             }
55              
56             sub uuid {
57 276     276 1 366 my $self = shift;
58 276 100 100     843 if (@_ || !defined $self->{uuid}) {
59 84 100       246 my %args = @_ % 2 == 1 ? (uuid => shift, @_) : @_;
60 84         125 my $old_uuid = $self->{uuid};
61 84   66     312 my $uuid = $self->{uuid} = delete $args{uuid} // generate_uuid;
62 84 100       220 $self->_signal('uuid.changed', $uuid, $old_uuid) if defined $old_uuid;
63             }
64 276         779 $self->{uuid};
65             }
66              
67             ##############################################################################
68              
69              
70             sub entries {
71 843     843 1 106591 my $self = shift;
72 843   100     2024 my $entries = $self->{entries} //= [];
73 843 100 100     2125 if (@$entries && !blessed($entries->[0])) {
74 12         44 @$entries = map { $self->_wrap_entry($_, $self->kdbx) } @$entries;
  12         41  
75             }
76 843     0   2788 assert { !any { !blessed $_ } @$entries };
  0         0  
  0         0  
77 843         2536 return $entries;
78             }
79              
80              
81             sub all_entries {
82 210     210 1 324 my $self = shift;
83 210         431 my %args = @_;
84              
85 210         308 my $searching = delete $args{searching};
86 210         300 my $auto_type = delete $args{auto_type};
87 210         301 my $history = delete $args{history};
88              
89 210         417 my $groups = $self->all_groups(%args);
90 210         1272 my @entries;
91              
92             return File::KDBX::Iterator->new(sub {
93 341 100   341   700 if (!@entries) {
94 288         615 while (my $group = $groups->next) {
95 317 100 100     625 next if $searching && !$group->effective_enable_searching;
96 316 50 66     587 next if $auto_type && !$group->effective_enable_auto_type;
97 316         384 @entries = @{$group->entries};
  316         540  
98 316 100       563 @entries = grep { $_->auto_type->{enabled} } @entries if $auto_type;
  1         4  
99 316 100       588 @entries = map { ($_, @{$_->history}) } @entries if $history;
  43         72  
  43         136  
100 316 100       796 last if @entries;
101             }
102             }
103 341         1507 shift @entries;
104 210         860 });
105             }
106              
107              
108             sub add_entry {
109 30     30 1 64 my $self = shift;
110 30 100       85 my $entry = @_ % 2 == 1 ? shift : undef;
111 30         78 my %args = @_;
112              
113 30   66     81 my $kdbx = delete $args{kdbx} // eval { $self->kdbx };
  10         25  
114              
115 30   100     173 $entry = $self->_wrap_entry($entry // [%args]);
116 30         117 $entry->uuid;
117 30 50       116 $entry->kdbx($kdbx) if $kdbx;
118              
119 30   50     44 push @{$self->{entries} ||= []}, $entry->remove;
  30         152  
120 30         81 return $entry->_set_group($self)->_signal('added', $self);
121             }
122              
123              
124             sub remove_entry {
125 4     4 1 7 my $self = shift;
126 4 50       18 my $uuid = is_ref($_[0]) ? $self->_wrap_entry(shift)->uuid : shift;
127 4         13 my %args = @_;
128 4         6 my $objects = $self->{entries};
129 4         20 for (my $i = 0; $i < @$objects; ++$i) {
130 4         8 my $object = $objects->[$i];
131 4 50       9 next if $uuid ne $object->uuid;
132 4         13 $object->_set_group(undef);
133 4 100 100     21 $object->_signal('removed') if $args{signal} // 1;
134 4         16 return splice @$objects, $i, 1;
135             }
136             }
137              
138             ##############################################################################
139              
140              
141             sub groups {
142 1226     1226 1 1510 my $self = shift;
143 1226   100     2316 my $groups = $self->{groups} //= [];
144 1226 100 100     3228 if (@$groups && !blessed($groups->[0])) {
145 7         18 @$groups = map { $self->_wrap_group($_, $self->kdbx) } @$groups;
  32         64  
146             }
147 1226     0   4233 assert { !any { !blessed $_ } @$groups };
  0         0  
  0         0  
148 1226         3679 return $groups;
149             }
150              
151              
152             sub all_groups {
153 454     454 1 586 my $self = shift;
154 454         608 my %args = @_;
155              
156 454 50 50     1664 my @groups = ($args{inclusive} // 1) ? $self : @{$self->groups};
  0         0  
157 454   100     1284 my $algo = lc($args{algorithm} || 'ids');
158              
159 454 100       1079 if ($algo eq ITERATION_DFS) {
    100          
160 4         5 my %visited;
161             return File::KDBX::Iterator->new(sub {
162 21 100   21   44 my $next = shift @groups or return;
163 17 100       54 if (!$visited{Hash::Util::FieldHash::id($next)}++) {
164 9         15 while (my @children = @{$next->groups}) {
  17         32  
165 8         17 unshift @groups, @children, $next;
166 8         9 $next = shift @groups;
167 8         22 $visited{Hash::Util::FieldHash::id($next)}++;
168             }
169             }
170 17         42 $next;
171 4         24 });
172             }
173             elsif ($algo eq ITERATION_BFS) {
174             return File::KDBX::Iterator->new(sub {
175 15 100   15   30 my $next = shift @groups or return;
176 12         15 push @groups, @{$next->groups};
  12         21  
177 12         25 $next;
178 3         18 });
179             }
180             return File::KDBX::Iterator->new(sub {
181 1045 100   1045   2251 my $next = shift @groups or return;
182 642         802 unshift @groups, @{$next->groups};
  642         1024  
183 642         1729 $next;
184 447         2482 });
185             }
186              
187 0     0   0 sub _kpx_groups { shift->groups(@_) }
188              
189              
190             sub add_group {
191 19     19 1 40 my $self = shift;
192 19 100       57 my $group = @_ % 2 == 1 ? shift : undef;
193 19         48 my %args = @_;
194              
195 19   66     57 my $kdbx = delete $args{kdbx} // eval { $self->kdbx };
  4         15  
196              
197 19   100     132 $group = $self->_wrap_group($group // [%args]);
198 19         66 $group->uuid;
199 19 50       75 $group->kdbx($kdbx) if $kdbx;
200              
201 19   50     30 push @{$self->{groups} ||= []}, $group->remove;
  19         92  
202 19         59 return $group->_set_group($self)->_signal('added', $self);
203             }
204              
205              
206             sub remove_group {
207 3     3 1 4 my $self = shift;
208 3 50       13 my $uuid = is_ref($_[0]) ? $self->_wrap_group(shift)->uuid : shift;
209 3         6 my %args = @_;
210 3         5 my $objects = $self->{groups};
211 3         9 for (my $i = 0; $i < @$objects; ++$i) {
212 3         4 my $object = $objects->[$i];
213 3 50       5 next if $uuid ne $object->uuid;
214 3         10 $object->_set_group(undef);
215 3 50 50     14 $object->_signal('removed') if $args{signal} // 1;
216 3         13 return splice @$objects, $i, 1;
217             }
218             }
219              
220             ##############################################################################
221              
222              
223             sub all_objects {
224 108     108 1 186 my $self = shift;
225 108         182 my %args = @_;
226              
227 108         169 my $searching = delete $args{searching};
228 108         160 my $auto_type = delete $args{auto_type};
229 108         147 my $history = delete $args{history};
230              
231 108         240 my $groups = $self->all_groups(%args);
232 108         683 my @entries;
233              
234             return File::KDBX::Iterator->new(sub {
235 288 100   288   530 if (!@entries) {
236 251         530 while (my $group = $groups->next) {
237 154 50 33     355 next if $searching && !$group->effective_enable_searching;
238 154 50 33     332 next if $auto_type && !$group->effective_enable_auto_type;
239 154         179 @entries = @{$group->entries};
  154         287  
240 154 50       300 @entries = grep { $_->auto_type->{enabled} } @entries if $auto_type;
  0         0  
241 154 100       259 @entries = map { ($_, @{$_->history}) } @entries if $history;
  1         11  
  1         3  
242 154         427 return $group;
243             }
244             }
245 134         344 shift @entries;
246 108         446 });
247             }
248              
249              
250             sub add_object {
251 4     4 1 7 my $self = shift;
252 4         7 my $obj = shift;
253 4 100       23 if ($obj->isa('File::KDBX::Entry')) {
    50          
254 3         9 $self->add_entry($obj);
255             }
256             elsif ($obj->isa('File::KDBX::Group')) {
257 1         6 $self->add_group($obj);
258             }
259             }
260              
261              
262             sub remove_object {
263 7     7 1 11 my $self = shift;
264 7         8 my $object = shift;
265 7         15 my $blessed = blessed($object);
266 7 100 66     60 return $self->remove_group($object, @_) if $blessed && $object->isa('File::KDBX::Group');
267 4 50 33     24 return $self->remove_entry($object, @_) if $blessed && $object->isa('File::KDBX::Entry');
268 0   0     0 return $self->remove_group($object, @_) || $self->remove_entry($object, @_);
269             }
270              
271             ##############################################################################
272              
273              
274             sub effective_default_auto_type_sequence {
275 0     0 1 0 my $self = shift;
276 0         0 my $sequence = $self->default_auto_type_sequence;
277 0 0       0 return $sequence if defined $sequence;
278              
279 0 0       0 my $parent = $self->group or return '{USERNAME}{TAB}{PASSWORD}{ENTER}';
280 0         0 return $parent->effective_default_auto_type_sequence;
281             }
282              
283              
284             sub effective_enable_auto_type {
285 1     1 1 2 my $self = shift;
286 1         3 my $enabled = $self->enable_auto_type;
287 1 50       3 return $enabled if defined $enabled;
288              
289 1 50       5 my $parent = $self->group or return true;
290 0         0 return $parent->effective_enable_auto_type;
291             }
292              
293              
294             sub effective_enable_searching {
295 2     2 1 2 my $self = shift;
296 2         6 my $enabled = $self->enable_searching;
297 2 100       10 return $enabled if defined $enabled;
298              
299 1 50       3 my $parent = $self->group or return true;
300 0         0 return $parent->effective_enable_searching;
301             }
302              
303             ##############################################################################
304              
305              
306             sub is_empty {
307 5     5 1 7 my $self = shift;
308 5   100     6 return @{$self->groups} == 0 && @{$self->entries} == 0;
309             }
310              
311              
312             sub is_root {
313 8     8 1 14 my $self = shift;
314 8 50       11 my $kdbx = eval { $self->kdbx } or return FALSE;
  8         23  
315 8         23 return Hash::Util::FieldHash::id($kdbx->root) == Hash::Util::FieldHash::id($self);
316             }
317              
318              
319             sub is_recycle_bin {
320 5     5 1 8 my $self = shift;
321 5 50       8 my $kdbx = eval { $self->kdbx } or return FALSE;
  5         10  
322 5         14 my $group = $kdbx->recycle_bin;
323 5   66     41 return $group && Hash::Util::FieldHash::id($group) == Hash::Util::FieldHash::id($self);
324             }
325              
326              
327             sub is_entry_templates {
328 0     0 1 0 my $self = shift;
329 0 0       0 my $kdbx = eval { $self->kdbx } or return FALSE;
  0         0  
330 0         0 my $group = $kdbx->entry_templates;
331 0   0     0 return $group && Hash::Util::FieldHash::id($group) == Hash::Util::FieldHash::id($self);
332             }
333              
334              
335             sub is_last_selected {
336 0     0 1 0 my $self = shift;
337 0 0       0 my $kdbx = eval { $self->kdbx } or return FALSE;
  0         0  
338 0         0 my $group = $kdbx->last_selected;
339 0   0     0 return $group && Hash::Util::FieldHash::id($group) == Hash::Util::FieldHash::id($self);
340             }
341              
342              
343             sub is_last_top_visible {
344 0     0 1 0 my $self = shift;
345 0 0       0 my $kdbx = eval { $self->kdbx } or return FALSE;
  0         0  
346 0         0 my $group = $kdbx->last_top_visible;
347 0   0     0 return $group && Hash::Util::FieldHash::id($group) == Hash::Util::FieldHash::id($self);
348             }
349              
350              
351             sub path {
352 3     3 1 8 my $self = shift;
353 3 100       21 return $self->name if $self->is_root;
354 2 50       11 my $lineage = $self->lineage or return;
355 2         7 my @parts = (@$lineage, $self);
356 2         5 shift @parts;
357 2         6 return join('.', map { $_->name } @parts);
  3         9  
358             }
359              
360              
361             sub size {
362 0     0 1 0 my $self = shift;
363 0         0 return sum0 map { $_->size } @{$self->groups}, @{$self->entries};
  0         0  
  0         0  
  0         0  
364             }
365              
366              
367 0 0 0 0 1 0 sub depth { $_[0]->is_root ? 0 : (scalar @{$_[0]->lineage || []} || -1) }
368              
369             sub _signal {
370 23     23   35 my $self = shift;
371 23         33 my $type = shift;
372 23         100 return $self->SUPER::_signal("group.$type", @_);
373             }
374              
375             sub _commit {
376 6     6   8 my $self = shift;
377 6         17 my $time = gmtime;
378 6         349 $self->last_modification_time($time);
379 6         69 $self->last_access_time($time);
380             }
381              
382 65     65 1 221 sub label { shift->name(@_) }
383              
384             ### Name of the parent attribute expected to contain the object
385 9     9   19 sub _parent_container { 'groups' }
386              
387             1;
388              
389             __END__