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   102959 use warnings;
  9         16  
  9         307  
5 9     9   41 use strict;
  9         13  
  9         184  
6              
7 9     9   424 use Devel::GlobalDestruction;
  9         546  
  9         69  
8 9     9   621 use File::KDBX::Constants qw(:bool :icon :iteration);
  9         18  
  9         3629  
9 9     9   68 use File::KDBX::Error;
  9         13  
  9         401  
10 9     9   3724 use File::KDBX::Iterator;
  9         25  
  9         293  
11 9     9   55 use File::KDBX::Util qw(:assert :class :coercion generate_uuid);
  9         16  
  9         1333  
12 9     9   708 use Hash::Util::FieldHash;
  9         2390  
  9         322  
13 9     9   48 use List::Util qw(any sum0);
  9         17  
  9         475  
14 9     9   50 use Ref::Util qw(is_coderef is_ref);
  9         26  
  9         328  
15 9     9   54 use Scalar::Util qw(blessed);
  9         14  
  9         389  
16 9     9   54 use Time::Piece 1.33;
  9         201  
  9         61  
17 9     9   687 use boolean;
  9         17  
  9         54  
18 9     9   471 use namespace::clean;
  9         15  
  9         54  
19              
20             extends 'File::KDBX::Object';
21              
22             our $VERSION = '0.904'; # VERSION
23 230 50   230 1 721  
24 230 100   149 1 755  
  149 50       492  
25 230 100 66 237 1 916 # has uuid => sub { generate_uuid(printable => 1) };
  149 50       470  
  237         604  
26 149 100 66 103 1 586 has name => '', coerce => \&to_string;
  237 50       515  
  103         676  
27 237 100 66 88 1 1247 has notes => '', coerce => \&to_string;
  103 50       291  
  88         304  
28 103 50 66 148 1 636 has tags => '', coerce => \&to_string;
  88 50       214  
  148         463  
29 88 100 50 84 1 371 has icon_id => ICON_FOLDER, coerce => \&to_icon_constant;
  148 50       527  
  84         744  
30 148 50 66 85 1 1032 has custom_icon_uuid => undef, coerce => \&to_uuid;
  84 50       207  
  85         275  
31 84 100 66 86 1 499 has is_expanded => false, coerce => \&to_bool;
  85 50       242  
  86         808  
32 85 100 100 80 1 430 has default_auto_type_sequence => '', coerce => \&to_string;
  86 50       312  
  80         239  
33 86 50 100     498 has enable_auto_type => undef, coerce => \&to_tristate;
  80         188  
34 80 50 50 238 1 372 has enable_searching => undef, coerce => \&to_tristate;
  238         773  
35 238 100       504 has last_top_visible_entry => undef, coerce => \&to_uuid;
36 238   100     1228 # has custom_data => {};
37 668 50   668 0 1384 has previous_parent_group => undef, coerce => \&to_uuid;
38 668 50       1063 # has entries => [];
39 668 50 100 86 1 3103 # has groups => [];
  86         296  
40 86 100   80 1 204 has times => {};
  80 50       5677  
41 86 50 100 86 1 173  
  80 50       214  
  86         4681  
42 80 100 50 80 1 184 has last_modification_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  86 50       231  
  80         4596  
43 86 50 100 84 1 198 has creation_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  80 50       216  
  84         4652  
44 80 50 50 80 1 206 has last_access_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  84 50       273  
  80         680  
45 84 50 66 80 1 188 has expiry_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  80 50       186  
  80         240  
46 80 50 33     171 has expires => false, store => 'times', coerce => \&to_bool;
  80         184  
47 80   50     174 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 80     80   134 my $self = shift;
53 80         374 $self->$_ for @ATTRS, list_attributes(ref $self);
54             }
55              
56             sub uuid {
57 273     273 1 406 my $self = shift;
58 273 100 100     909 if (@_ || !defined $self->{uuid}) {
59 81 100       295 my %args = @_ % 2 == 1 ? (uuid => shift, @_) : @_;
60 81         151 my $old_uuid = $self->{uuid};
61 81   66     358 my $uuid = $self->{uuid} = delete $args{uuid} // generate_uuid;
62 81 100       236 $self->_signal('uuid.changed', $uuid, $old_uuid) if defined $old_uuid;
63             }
64 273         747 $self->{uuid};
65             }
66              
67             ##############################################################################
68              
69              
70             sub entries {
71 830     830 1 105729 my $self = shift;
72 830   100     1854 my $entries = $self->{entries} //= [];
73 830 100 100     2113 if (@$entries && !blessed($entries->[0])) {
74 12         39 @$entries = map { $self->_wrap_entry($_, $self->kdbx) } @$entries;
  12         39  
75             }
76 830     0   2944 assert { !any { !blessed $_ } @$entries };
  0         0  
  0         0  
77 830         2550 return $entries;
78             }
79              
80              
81             sub all_entries {
82 205     205 1 301 my $self = shift;
83 205         514 my %args = @_;
84              
85 205         383 my $searching = delete $args{searching};
86 205         429 my $auto_type = delete $args{auto_type};
87 205         323 my $history = delete $args{history};
88              
89 205         594 my $groups = $self->all_groups(%args);
90 205         1240 my @entries;
91              
92             return File::KDBX::Iterator->new(sub {
93 335 100   335   716 if (!@entries) {
94 282         620 while (my $group = $groups->next) {
95 312 100 100     703 next if $searching && !$group->effective_enable_searching;
96 311 50 66     626 next if $auto_type && !$group->effective_enable_auto_type;
97 311         479 @entries = @{$group->entries};
  311         646  
98 311 100       582 @entries = grep { $_->auto_type->{enabled} } @entries if $auto_type;
  1         6  
99 311 100       650 @entries = map { ($_, @{$_->history}) } @entries if $history;
  42         76  
  42         155  
100 311 100       840 last if @entries;
101             }
102             }
103 335         1557 shift @entries;
104 205         965 });
105             }
106              
107              
108             sub add_entry {
109 29     29 1 59 my $self = shift;
110 29 100       78 my $entry = @_ % 2 == 1 ? shift : undef;
111 29         68 my %args = @_;
112              
113 29   66     86 my $kdbx = delete $args{kdbx} // eval { $self->kdbx };
  9         18  
114              
115 29   100     173 $entry = $self->_wrap_entry($entry // [%args]);
116 29         98 $entry->uuid;
117 29 50       110 $entry->kdbx($kdbx) if $kdbx;
118              
119 29   50     46 push @{$self->{entries} ||= []}, $entry->remove;
  29         137  
120 29         74 return $entry->_set_group($self)->_signal('added', $self);
121             }
122              
123              
124             sub remove_entry {
125 4     4 1 6 my $self = shift;
126 4 50       13 my $uuid = is_ref($_[0]) ? $self->_wrap_entry(shift)->uuid : shift;
127 4         9 my %args = @_;
128 4         6 my $objects = $self->{entries};
129 4         23 for (my $i = 0; $i < @$objects; ++$i) {
130 4         7 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     17 $object->_signal('removed') if $args{signal} // 1;
134 4         12 return splice @$objects, $i, 1;
135             }
136             }
137              
138             ##############################################################################
139              
140              
141             sub groups {
142 1207     1207 1 1473 my $self = shift;
143 1207   100     2435 my $groups = $self->{groups} //= [];
144 1207 100 100     2933 if (@$groups && !blessed($groups->[0])) {
145 7         24 @$groups = map { $self->_wrap_group($_, $self->kdbx) } @$groups;
  32         66  
146             }
147 1207     0   4503 assert { !any { !blessed $_ } @$groups };
  0         0  
  0         0  
148 1207         3708 return $groups;
149             }
150              
151              
152             sub all_groups {
153 444     444 1 596 my $self = shift;
154 444         723 my %args = @_;
155              
156 444 50 50     1689 my @groups = ($args{inclusive} // 1) ? $self : @{$self->groups};
  0         0  
157 444   100     1433 my $algo = lc($args{algorithm} || 'ids');
158              
159 444 100       1170 if ($algo eq ITERATION_DFS) {
    100          
160 4         6 my %visited;
161             return File::KDBX::Iterator->new(sub {
162 21 100   21   45 my $next = shift @groups or return;
163 17 100       52 if (!$visited{Hash::Util::FieldHash::id($next)}++) {
164 9         15 while (my @children = @{$next->groups}) {
  17         25  
165 8         18 unshift @groups, @children, $next;
166 8         10 $next = shift @groups;
167 8         19 $visited{Hash::Util::FieldHash::id($next)}++;
168             }
169             }
170 17         37 $next;
171 4         24 });
172             }
173             elsif ($algo eq ITERATION_BFS) {
174             return File::KDBX::Iterator->new(sub {
175 15 100   15   31 my $next = shift @groups or return;
176 12         15 push @groups, @{$next->groups};
  12         17  
177 12         25 $next;
178 3         16 });
179             }
180             return File::KDBX::Iterator->new(sub {
181 1025 100   1025   2411 my $next = shift @groups or return;
182 632         864 unshift @groups, @{$next->groups};
  632         1125  
183 632         1843 $next;
184 437         2619 });
185             }
186              
187 0     0   0 sub _kpx_groups { shift->groups(@_) }
188              
189              
190             sub add_group {
191 19     19 1 37 my $self = shift;
192 19 100       48 my $group = @_ % 2 == 1 ? shift : undef;
193 19         48 my %args = @_;
194              
195 19   66     55 my $kdbx = delete $args{kdbx} // eval { $self->kdbx };
  4         32  
196              
197 19   100     114 $group = $self->_wrap_group($group // [%args]);
198 19         61 $group->uuid;
199 19 50       72 $group->kdbx($kdbx) if $kdbx;
200              
201 19   50     28 push @{$self->{groups} ||= []}, $group->remove;
  19         92  
202 19         45 return $group->_set_group($self)->_signal('added', $self);
203             }
204              
205              
206             sub remove_group {
207 3     3 1 7 my $self = shift;
208 3 50       9 my $uuid = is_ref($_[0]) ? $self->_wrap_group(shift)->uuid : shift;
209 3         7 my %args = @_;
210 3         4 my $objects = $self->{groups};
211 3         7 for (my $i = 0; $i < @$objects; ++$i) {
212 3         5 my $object = $objects->[$i];
213 3 50       6 next if $uuid ne $object->uuid;
214 3         8 $object->_set_group(undef);
215 3 50 50     14 $object->_signal('removed') if $args{signal} // 1;
216 3         9 return splice @$objects, $i, 1;
217             }
218             }
219              
220             ##############################################################################
221              
222              
223             sub all_objects {
224 106     106 1 159 my $self = shift;
225 106         188 my %args = @_;
226              
227 106         217 my $searching = delete $args{searching};
228 106         150 my $auto_type = delete $args{auto_type};
229 106         178 my $history = delete $args{history};
230              
231 106         268 my $groups = $self->all_groups(%args);
232 106         650 my @entries;
233              
234             return File::KDBX::Iterator->new(sub {
235 284 100   284   523 if (!@entries) {
236 247         485 while (my $group = $groups->next) {
237 152 50 33     383 next if $searching && !$group->effective_enable_searching;
238 152 50 33     313 next if $auto_type && !$group->effective_enable_auto_type;
239 152         223 @entries = @{$group->entries};
  152         298  
240 152 50       329 @entries = grep { $_->auto_type->{enabled} } @entries if $auto_type;
  0         0  
241 152 100       274 @entries = map { ($_, @{$_->history}) } @entries if $history;
  1         2  
  1         3  
242 152         422 return $group;
243             }
244             }
245 132         364 shift @entries;
246 106         464 });
247             }
248              
249              
250             sub add_object {
251 4     4 1 6 my $self = shift;
252 4         7 my $obj = shift;
253 4 100       18 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 7 my $self = shift;
264 7         9 my $object = shift;
265 7         14 my $blessed = blessed($object);
266 7 100 66     39 return $self->remove_group($object, @_) if $blessed && $object->isa('File::KDBX::Group');
267 4 50 33     22 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 3 my $self = shift;
286 1         4 my $enabled = $self->enable_auto_type;
287 1 50       5 return $enabled if defined $enabled;
288              
289 1 50       6 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 4 my $self = shift;
296 2         5 my $enabled = $self->enable_searching;
297 2 100       10 return $enabled if defined $enabled;
298              
299 1 50       4 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 9 my $self = shift;
308 5   100     5 return @{$self->groups} == 0 && @{$self->entries} == 0;
309             }
310              
311              
312             sub is_root {
313 8     8 1 12 my $self = shift;
314 8 50       10 my $kdbx = eval { $self->kdbx } or return FALSE;
  8         18  
315 8         28 return Hash::Util::FieldHash::id($kdbx->root) == Hash::Util::FieldHash::id($self);
316             }
317              
318              
319             sub is_recycle_bin {
320 5     5 1 7 my $self = shift;
321 5 50       6 my $kdbx = eval { $self->kdbx } or return FALSE;
  5         11  
322 5         11 my $group = $kdbx->recycle_bin;
323 5   66     37 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 5 my $self = shift;
353 3 100       7 return $self->name if $self->is_root;
354 2 50       9 my $lineage = $self->lineage or return;
355 2         6 my @parts = (@$lineage, $self);
356 2         2 shift @parts;
357 2         4 return join('.', map { $_->name } @parts);
  3         6  
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   37 my $self = shift;
371 23         37 my $type = shift;
372 23         96 return $self->SUPER::_signal("group.$type", @_);
373             }
374              
375             sub _commit {
376 6     6   10 my $self = shift;
377 6         15 my $time = gmtime;
378 6         334 $self->last_modification_time($time);
379 6         69 $self->last_access_time($time);
380             }
381              
382 65     65 1 223 sub label { shift->name(@_) }
383              
384             ### Name of the parent attribute expected to contain the object
385 9     9   15 sub _parent_container { 'groups' }
386              
387             1;
388              
389             __END__