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   88767 use warnings;
  9         16  
  9         306  
5 9     9   46 use strict;
  9         15  
  9         186  
6              
7 9     9   401 use Devel::GlobalDestruction;
  9         490  
  9         76  
8 9     9   618 use File::KDBX::Constants qw(:bool :icon :iteration);
  9         27  
  9         3684  
9 9     9   62 use File::KDBX::Error;
  9         12  
  9         427  
10 9     9   3662 use File::KDBX::Iterator;
  9         20  
  9         301  
11 9     9   53 use File::KDBX::Util qw(:assert :class :coercion generate_uuid);
  9         16  
  9         1273  
12 9     9   500 use Hash::Util::FieldHash;
  9         1795  
  9         296  
13 9     9   42 use List::Util qw(any sum0);
  9         21  
  9         460  
14 9     9   65 use Ref::Util qw(is_coderef is_ref);
  9         17  
  9         344  
15 9     9   44 use Scalar::Util qw(blessed);
  9         17  
  9         332  
16 9     9   44 use Time::Piece 1.33;
  9         222  
  9         58  
17 9     9   698 use boolean;
  9         14  
  9         62  
18 9     9   504 use namespace::clean;
  9         15  
  9         41  
19              
20             extends 'File::KDBX::Object';
21              
22             our $VERSION = '0.905'; # VERSION
23 230 50   230 1 627  
24 230 100   149 1 601  
  149 50       419  
25 230 100 66 237 1 838 # has uuid => sub { generate_uuid(printable => 1) };
  149 50       407  
  237         606  
26 149 100 66 103 1 520 has name => '', coerce => \&to_string;
  237 50       437  
  103         659  
27 237 100 66 88 1 1056 has notes => '', coerce => \&to_string;
  103 50       235  
  88         228  
28 103 50 66 148 1 572 has tags => '', coerce => \&to_string;
  88 50       163  
  148         386  
29 88 100 50 84 1 325 has icon_id => ICON_FOLDER, coerce => \&to_icon_constant;
  148 50       395  
  84         584  
30 148 50 66 85 1 922 has custom_icon_uuid => undef, coerce => \&to_uuid;
  84 50       175  
  85         233  
31 84 100 66 86 1 342 has is_expanded => false, coerce => \&to_bool;
  85 50       184  
  86         783  
32 85 100 100 80 1 337 has default_auto_type_sequence => '', coerce => \&to_string;
  86 50       259  
  80         203  
33 86 50 100     402 has enable_auto_type => undef, coerce => \&to_tristate;
  80         157  
34 80 50 50 238 1 296 has enable_searching => undef, coerce => \&to_tristate;
  238         599  
35 238 100       492 has last_top_visible_entry => undef, coerce => \&to_uuid;
36 238   100     1014 # has custom_data => {};
37 668 50   668 0 1293 has previous_parent_group => undef, coerce => \&to_uuid;
38 668 50       1080 # has entries => [];
39 668 50 100 86 1 2736 # has groups => [];
  86         260  
40 86 100   80 1 187 has times => {};
  80 50       5443  
41 86 50 100 86 1 144  
  80 50       206  
  86         4572  
42 80 100 50 80 1 170 has last_modification_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  86 50       191  
  80         4465  
43 86 50 100 84 1 171 has creation_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  80 50       183  
  84         4467  
44 80 50 50 80 1 160 has last_access_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  84 50       177  
  80         537  
45 84 50 66 80 1 159 has expiry_time => sub { gmtime }, store => 'times', coerce => \&to_time;
  80 50       201  
  80         214  
46 80 50 33     141 has expires => false, store => 'times', coerce => \&to_bool;
  80         170  
47 80   50     125 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   109 my $self = shift;
53 80         316 $self->$_ for @ATTRS, list_attributes(ref $self);
54             }
55              
56             sub uuid {
57 273     273 1 352 my $self = shift;
58 273 100 100     850 if (@_ || !defined $self->{uuid}) {
59 81 100       216 my %args = @_ % 2 == 1 ? (uuid => shift, @_) : @_;
60 81         124 my $old_uuid = $self->{uuid};
61 81   66     333 my $uuid = $self->{uuid} = delete $args{uuid} // generate_uuid;
62 81 100       199 $self->_signal('uuid.changed', $uuid, $old_uuid) if defined $old_uuid;
63             }
64 273         676 $self->{uuid};
65             }
66              
67             ##############################################################################
68              
69              
70             sub entries {
71 830     830 1 104841 my $self = shift;
72 830   100     1667 my $entries = $self->{entries} //= [];
73 830 100 100     2061 if (@$entries && !blessed($entries->[0])) {
74 12         31 @$entries = map { $self->_wrap_entry($_, $self->kdbx) } @$entries;
  12         36  
75             }
76 830     0   2883 assert { !any { !blessed $_ } @$entries };
  0         0  
  0         0  
77 830         2432 return $entries;
78             }
79              
80              
81             sub all_entries {
82 205     205 1 268 my $self = shift;
83 205         384 my %args = @_;
84              
85 205         331 my $searching = delete $args{searching};
86 205         391 my $auto_type = delete $args{auto_type};
87 205         280 my $history = delete $args{history};
88              
89 205         415 my $groups = $self->all_groups(%args);
90 205         1246 my @entries;
91              
92             return File::KDBX::Iterator->new(sub {
93 335 100   335   748 if (!@entries) {
94 282         590 while (my $group = $groups->next) {
95 312 100 100     635 next if $searching && !$group->effective_enable_searching;
96 311 50 66     572 next if $auto_type && !$group->effective_enable_auto_type;
97 311         359 @entries = @{$group->entries};
  311         544  
98 311 100       591 @entries = grep { $_->auto_type->{enabled} } @entries if $auto_type;
  1         3  
99 311 100       554 @entries = map { ($_, @{$_->history}) } @entries if $history;
  42         66  
  42         123  
100 311 100       771 last if @entries;
101             }
102             }
103 335         1450 shift @entries;
104 205         873 });
105             }
106              
107              
108             sub add_entry {
109 29     29 1 53 my $self = shift;
110 29 100       323 my $entry = @_ % 2 == 1 ? shift : undef;
111 29         77 my %args = @_;
112              
113 29   66     76 my $kdbx = delete $args{kdbx} // eval { $self->kdbx };
  9         19  
114              
115 29   100     179 $entry = $self->_wrap_entry($entry // [%args]);
116 29         96 $entry->uuid;
117 29 50       99 $entry->kdbx($kdbx) if $kdbx;
118              
119 29   50     39 push @{$self->{entries} ||= []}, $entry->remove;
  29         118  
120 29         64 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       12 my $uuid = is_ref($_[0]) ? $self->_wrap_entry(shift)->uuid : shift;
127 4         8 my %args = @_;
128 4         6 my $objects = $self->{entries};
129 4         7 for (my $i = 0; $i < @$objects; ++$i) {
130 4         6 my $object = $objects->[$i];
131 4 50       7 next if $uuid ne $object->uuid;
132 4         20 $object->_set_group(undef);
133 4 100 100     17 $object->_signal('removed') if $args{signal} // 1;
134 4         13 return splice @$objects, $i, 1;
135             }
136             }
137              
138             ##############################################################################
139              
140              
141             sub groups {
142 1207     1207 1 1397 my $self = shift;
143 1207   100     2431 my $groups = $self->{groups} //= [];
144 1207 100 100     2623 if (@$groups && !blessed($groups->[0])) {
145 7         32 @$groups = map { $self->_wrap_group($_, $self->kdbx) } @$groups;
  32         61  
146             }
147 1207     0   4227 assert { !any { !blessed $_ } @$groups };
  0         0  
  0         0  
148 1207         3520 return $groups;
149             }
150              
151              
152             sub all_groups {
153 444     444 1 579 my $self = shift;
154 444         612 my %args = @_;
155              
156 444 50 50     1670 my @groups = ($args{inclusive} // 1) ? $self : @{$self->groups};
  0         0  
157 444   100     1268 my $algo = lc($args{algorithm} || 'ids');
158              
159 444 100       1183 if ($algo eq ITERATION_DFS) {
    100          
160 4         5 my %visited;
161             return File::KDBX::Iterator->new(sub {
162 21 100   21   49 my $next = shift @groups or return;
163 17 100       76 if (!$visited{Hash::Util::FieldHash::id($next)}++) {
164 9         13 while (my @children = @{$next->groups}) {
  17         27  
165 8         15 unshift @groups, @children, $next;
166 8         12 $next = shift @groups;
167 8         18 $visited{Hash::Util::FieldHash::id($next)}++;
168             }
169             }
170 17         40 $next;
171 4         21 });
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         12 push @groups, @{$next->groups};
  12         22  
177 12         25 $next;
178 3         15 });
179             }
180             return File::KDBX::Iterator->new(sub {
181 1025 100   1025   2261 my $next = shift @groups or return;
182 632         814 unshift @groups, @{$next->groups};
  632         1097  
183 632         1618 $next;
184 437         2358 });
185             }
186              
187 0     0   0 sub _kpx_groups { shift->groups(@_) }
188              
189              
190             sub add_group {
191 19     19 1 35 my $self = shift;
192 19 100       43 my $group = @_ % 2 == 1 ? shift : undef;
193 19         46 my %args = @_;
194              
195 19   66     72 my $kdbx = delete $args{kdbx} // eval { $self->kdbx };
  4         11  
196              
197 19   100     111 $group = $self->_wrap_group($group // [%args]);
198 19         63 $group->uuid;
199 19 50       73 $group->kdbx($kdbx) if $kdbx;
200              
201 19   50     28 push @{$self->{groups} ||= []}, $group->remove;
  19         83  
202 19         42 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       10 my $uuid = is_ref($_[0]) ? $self->_wrap_group(shift)->uuid : shift;
209 3         6 my %args = @_;
210 3         4 my $objects = $self->{groups};
211 3         7 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         7 $object->_set_group(undef);
215 3 50 50     12 $object->_signal('removed') if $args{signal} // 1;
216 3         10 return splice @$objects, $i, 1;
217             }
218             }
219              
220             ##############################################################################
221              
222              
223             sub all_objects {
224 106     106 1 144 my $self = shift;
225 106         162 my %args = @_;
226              
227 106         170 my $searching = delete $args{searching};
228 106         138 my $auto_type = delete $args{auto_type};
229 106         134 my $history = delete $args{history};
230              
231 106         203 my $groups = $self->all_groups(%args);
232 106         632 my @entries;
233              
234             return File::KDBX::Iterator->new(sub {
235 284 100   284   508 if (!@entries) {
236 247         500 while (my $group = $groups->next) {
237 152 50 33     299 next if $searching && !$group->effective_enable_searching;
238 152 50 33     274 next if $auto_type && !$group->effective_enable_auto_type;
239 152         179 @entries = @{$group->entries};
  152         259  
240 152 50       284 @entries = grep { $_->auto_type->{enabled} } @entries if $auto_type;
  0         0  
241 152 100       260 @entries = map { ($_, @{$_->history}) } @entries if $history;
  1         1  
  1         3  
242 152         434 return $group;
243             }
244             }
245 132         315 shift @entries;
246 106         392 });
247             }
248              
249              
250             sub add_object {
251 4     4 1 8 my $self = shift;
252 4         5 my $obj = shift;
253 4 100       18 if ($obj->isa('File::KDBX::Entry')) {
    50          
254 3         7 $self->add_entry($obj);
255             }
256             elsif ($obj->isa('File::KDBX::Group')) {
257 1         4 $self->add_group($obj);
258             }
259             }
260              
261              
262             sub remove_object {
263 7     7 1 9 my $self = shift;
264 7         10 my $object = shift;
265 7         12 my $blessed = blessed($object);
266 7 100 66     38 return $self->remove_group($object, @_) if $blessed && $object->isa('File::KDBX::Group');
267 4 50 33     18 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       2 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 3 my $self = shift;
296 2         5 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 8 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 11 my $self = shift;
314 8 50       10 my $kdbx = eval { $self->kdbx } or return FALSE;
  8         27  
315 8         20 return Hash::Util::FieldHash::id($kdbx->root) == Hash::Util::FieldHash::id($self);
316             }
317              
318              
319             sub is_recycle_bin {
320 5     5 1 9 my $self = shift;
321 5 50       6 my $kdbx = eval { $self->kdbx } or return FALSE;
  5         9  
322 5         13 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 6 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         5 my @parts = (@$lineage, $self);
356 2         3 shift @parts;
357 2         4 return join('.', map { $_->name } @parts);
  3         7  
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   42 my $self = shift;
371 23         32 my $type = shift;
372 23         81 return $self->SUPER::_signal("group.$type", @_);
373             }
374              
375             sub _commit {
376 6     6   9 my $self = shift;
377 6         15 my $time = gmtime;
378 6         332 $self->last_modification_time($time);
379 6         88 $self->last_access_time($time);
380             }
381              
382 65     65 1 214 sub label { shift->name(@_) }
383              
384             ### Name of the parent attribute expected to contain the object
385 9     9   16 sub _parent_container { 'groups' }
386              
387             1;
388              
389             __END__