1 package File
::KDBX
::Group
;
2 # ABSTRACT: A KDBX database group
7 use Devel
::GlobalDestruction
;
8 use File
::KDBX
::Constants
qw(:bool :icon);
10 use File
::KDBX
::Iterator
;
11 use File
::KDBX
::Util
qw(:assert :class :coercion generate_uuid);
12 use Hash
::Util
::FieldHash
;
13 use List
::Util
qw(any sum0);
14 use Ref
::Util
qw(is_coderef is_ref);
15 use Scalar
::Util
qw(blessed);
20 extends
'File::KDBX::Object';
22 our $VERSION = '999.999'; # VERSION
24 sub _parent_container
{ 'groups' }
26 # has uuid => sub { generate_uuid(printable => 1) };
27 has name
=> '', coerce
=> \
&to_string
;
28 has notes
=> '', coerce
=> \
&to_string
;
29 has tags
=> '', coerce
=> \
&to_string
;
30 has icon_id
=> ICON_FOLDER
, coerce
=> \
&to_icon_constant
;
31 has custom_icon_uuid
=> undef, coerce
=> \
&to_uuid
;
32 has is_expanded
=> false
, coerce
=> \
&to_bool
;
33 has default_auto_type_sequence
=> '', coerce
=> \
&to_string
;
34 has enable_auto_type
=> undef, coerce
=> \
&to_tristate
;
35 has enable_searching
=> undef, coerce
=> \
&to_tristate
;
36 has last_top_visible_entry
=> undef, coerce
=> \
&to_uuid
;
37 # has custom_data => {};
38 has previous_parent_group
=> undef, coerce
=> \
&to_uuid
;
43 has last_modification_time
=> sub { gmtime }, store
=> 'times', coerce
=> \
&to_time
;
44 has creation_time
=> sub { gmtime }, store
=> 'times', coerce
=> \
&to_time
;
45 has last_access_time
=> sub { gmtime }, store
=> 'times', coerce
=> \
&to_time
;
46 has expiry_time
=> sub { gmtime }, store
=> 'times', coerce
=> \
&to_time
;
47 has expires
=> false
, store
=> 'times', coerce
=> \
&to_bool
;
48 has usage_count
=> 0, store
=> 'times', coerce
=> \
&to_number
;
49 has location_changed
=> sub { gmtime }, store
=> 'times', coerce
=> \
&to_time
;
51 my @ATTRS = qw(uuid custom_data entries groups);
52 sub _set_nonlazy_attributes
{
54 $self->$_ for @ATTRS, list_attributes
(ref $self);
59 if (@_ || !defined $self->{uuid
}) {
60 my %args = @_ % 2 == 1 ? (uuid
=> shift, @_) : @_;
61 my $old_uuid = $self->{uuid
};
62 my $uuid = $self->{uuid
} = delete $args{uuid
} // generate_uuid
;
63 $self->_signal('uuid.changed', $uuid, $old_uuid) if defined $old_uuid;
68 ##############################################################################
72 my $entries = $self->{entries
} //= [];
73 if (@$entries && !blessed
($entries->[0])) {
74 @$entries = map { $self->_wrap_entry($_, $self->kdbx) } @$entries;
76 assert
{ !any
{ !blessed
$_ } @$entries };
84 my $searching = delete $args{searching
};
85 my $auto_type = delete $args{auto_type
};
86 my $history = delete $args{history
};
88 my $groups = $self->groups_deeply(%args);
91 return File
::KDBX
::Iterator-
>new(sub {
93 while (my $group = $groups->next) {
94 next if $searching && !$group->effective_enable_searching;
95 next if $auto_type && !$group->effective_enable_auto_type;
96 @entries = @{$group->entries};
97 @entries = grep { $_->auto_type->{enabled
} } @entries if $auto_type;
98 @entries = map { ($_, @{$_->history}) } @entries if $history;
108 $entry = $group->add_entry($entry);
109 $entry = $group->add_entry(%entry_attributes);
111 Add an entry to a group
. If C
<$entry> already
has a parent group
, it will be removed from that group before
112 being added to C
<$group>.
118 my $entry = @_ % 2 == 1 ? shift : undef;
121 my $kdbx = delete $args{kdbx
} // eval { $self->kdbx };
123 $entry = $self->_wrap_entry($entry // [%args]);
125 $entry->kdbx($kdbx) if $kdbx;
127 push @{$self->{entries
} ||= []}, $entry->remove;
128 return $entry->_set_group($self)->_signal('added', $self);
133 my $uuid = is_ref
($_[0]) ? $self->_wrap_entry(shift)->uuid : shift;
135 my $objects = $self->{entries
};
136 for (my $i = 0; $i < @$objects; ++$i) {
137 my $object = $objects->[$i];
138 next if $uuid ne $object->uuid;
139 $object->_set_group(undef);
140 $object->_signal('removed') if $args{signal
} // 1;
141 return splice @$objects, $i, 1;
145 ##############################################################################
149 my $groups = $self->{groups
} //= [];
150 if (@$groups && !blessed
($groups->[0])) {
151 @$groups = map { $self->_wrap_group($_, $self->kdbx) } @$groups;
153 assert
{ !any
{ !blessed
$_ } @$groups };
161 my @groups = ($args{inclusive
} // 1) ? $self : @{$self->groups};
162 my $algo = lc($args{algorithm
} || 'ids');
164 if ($algo eq 'dfs') {
166 return File
::KDBX
::Iterator-
>new(sub {
167 my $next = shift @groups or return;
168 if (!$visited{Hash
::Util
::FieldHash
::id
($next)}++) {
169 while (my @children = @{$next->groups}) {
170 unshift @groups, @children, $next;
171 $next = shift @groups;
172 $visited{Hash
::Util
::FieldHash
::id
($next)}++;
178 elsif ($algo eq 'bfs') {
179 return File
::KDBX
::Iterator-
>new(sub {
180 my $next = shift @groups or return;
181 push @groups, @{$next->groups};
185 return File
::KDBX
::Iterator-
>new(sub {
186 my $next = shift @groups or return;
187 unshift @groups, @{$next->groups};
192 sub _kpx_groups
{ shift-
>groups(@_) }
196 $new_group = $group->add_group($new_group);
197 $new_group = $group->add_group(%group_attributes);
199 Add a group to a group
. If C
<$new_group> already
has a parent group
, it will be removed from that group before
200 being added to C
<$group>.
206 my $group = @_ % 2 == 1 ? shift : undef;
209 my $kdbx = delete $args{kdbx
} // eval { $self->kdbx };
211 $group = $self->_wrap_group($group // [%args]);
213 $group->kdbx($kdbx) if $kdbx;
215 push @{$self->{groups
} ||= []}, $group->remove;
216 return $group->_set_group($self)->_signal('added', $self);
221 my $uuid = is_ref
($_[0]) ? $self->_wrap_group(shift)->uuid : shift;
223 my $objects = $self->{groups
};
224 for (my $i = 0; $i < @$objects; ++$i) {
225 my $object = $objects->[$i];
226 next if $uuid ne $object->uuid;
227 $object->_set_group(undef);
228 $object->_signal('removed') if $args{signal
} // 1;
229 return splice @$objects, $i, 1;
233 ##############################################################################
239 my $searching = delete $args{searching
};
240 my $auto_type = delete $args{auto_type
};
241 my $history = delete $args{history
};
243 my $groups = $self->groups_deeply(%args);
246 return File
::KDBX
::Iterator-
>new(sub {
248 while (my $group = $groups->next) {
249 next if $searching && !$group->effective_enable_searching;
250 next if $auto_type && !$group->effective_enable_auto_type;
251 @entries = @{$group->entries};
252 @entries = grep { $_->auto_type->{enabled
} } @entries if $auto_type;
253 @entries = map { ($_, @{$_->history}) } @entries if $history;
263 $new_entry = $group->add_object($new_entry);
264 $new_group = $group->add_object($new_group);
266 Add an object
(either a L
<File
::KDBX
::Entry
> or a L
<File
::KDBX
::Group
>) to a group
. This
is the generic
267 equivalent of the object forms of L
</add_entry> and L</add_group
>.
274 if ($obj->isa('File::KDBX::Entry')) {
275 $self->add_entry($obj);
277 elsif ($obj->isa('File::KDBX::Group')) {
278 $self->add_group($obj);
282 =method remove_object
284 $group->remove_object($entry);
285 $group->remove_object($group);
287 Remove an object
(either a L
<File
::KDBX
::Entry
> or a L
<File
::KDBX
::Group
>) from a group
. This
is the generic
288 equivalent of the object forms of L
</remove_entry> and L</remove_group
>.
295 my $blessed = blessed
($object);
296 return $self->remove_group($object, @_) if $blessed && $object->isa('File::KDBX::Group');
297 return $self->remove_entry($object, @_) if $blessed && $object->isa('File::KDBX::Entry');
298 return $self->remove_group($object, @_) || $self->remove_entry($object, @_);
301 ##############################################################################
305 $bool = $group->is_root;
307 Determine
if a group
is the root group of its connected database
.
313 my $kdbx = eval { $self->kdbx } or return FALSE
;
314 return Hash
::Util
::FieldHash
::id
($kdbx->root) == Hash
::Util
::FieldHash
::id
($self);
317 =method is_recycle_bin
319 $bool = $group->is_recycle_bin;
321 Get whether
or not a group
is the recycle bin of its connected database
.
327 my $kdbx = eval { $self->kdbx } or return FALSE
;
328 my $group = $kdbx->recycle_bin;
329 return $group && Hash
::Util
::FieldHash
::id
($group) == Hash
::Util
::FieldHash
::id
($self);
332 =method is_entry_templates
334 $bool = $group->is_entry_templates;
336 Get whether
or not a group
is the group containing entry template of its connected database
.
340 sub entry_templates
{
342 my $kdbx = eval { $self->kdbx } or return FALSE
;
343 my $group = $kdbx->entry_templates;
344 return $group && Hash
::Util
::FieldHash
::id
($group) == Hash
::Util
::FieldHash
::id
($self);
347 =method is_last_selected
349 $bool = $group->is_last_selected;
351 Get whether
or not a group
is the prior selected group of its connected database
.
357 my $kdbx = eval { $self->kdbx } or return FALSE
;
358 my $group = $kdbx->last_selected;
359 return $group && Hash
::Util
::FieldHash
::id
($group) == Hash
::Util
::FieldHash
::id
($self);
362 =method is_last_top_visible
364 $bool = $group->is_last_top_visible;
366 Get whether
or not a group
is the latest top visible group of its connected database
.
370 sub last_top_visible
{
372 my $kdbx = eval { $self->kdbx } or return FALSE
;
373 my $group = $kdbx->last_top_visible;
374 return $group && Hash
::Util
::FieldHash
::id
($group) == Hash
::Util
::FieldHash
::id
($self);
379 $string = $group->path;
381 Get a string representation of a group
's lineage. This is used as the substitution value for the
382 C<{GROUP_PATH}> placeholder. See L<File::KDBX::Entry/Placeholders>.
384 For a root group, the path is simply the name of the group. For deeper groups, the path is a period-separated
385 sequence of group names between the root group and C<$group>, including C<$group> but I<not> the root group.
386 In other words, paths of deeper groups leave the root group name out.
389 -> Root # path is "Root"
390 -> Foo # path is "Foo"
391 -> Bar # path is "Foo.Bar"
393 Yeah, it doesn't make much sense to me
, either
, but this matches the behavior of KeePass
.
399 return $self->name if $self->is_root;
400 my $lineage = $self->lineage or return;
401 my @parts = (@$lineage, $self);
403 return join('.', map { $_->name } @parts);
408 $size = $group->size;
410 Get the size
(in bytes
) of a group
, including the size of all subroups
and entries
, if any
.
416 return sum0
map { $_->size } @{$self->groups}, @{$self->entries};
421 $depth = $group->depth;
423 Get the depth of a group within a database
. The root group
is at depth
0, its direct children are at depth
1,
424 etc
. A group
not in a database tree structure returns a depth of
-1.
428 sub depth
{ $_[0]->is_root ? 0 : (scalar @{$_[0]->lineage || []} || -1) }
430 sub label
{ shift-
>name(@_) }
435 return $self->SUPER::_signal
("group.$type", @_);
441 $self->last_modification_time($time);
442 $self->last_access_time($time);
445 sub effective_default_auto_type_sequence
{
447 my $sequence = $self->default_auto_type_sequence;
448 return $sequence if defined $sequence;
450 my $parent = $self->parent or return '{USERNAME}{TAB}{PASSWORD}{ENTER}';
451 return $parent->effective_default_auto_type_sequence;
454 sub effective_enable_auto_type
{
456 my $enabled = $self->enable_auto_type;
457 return $enabled if defined $enabled;
459 my $parent = $self->parent or return true
;
460 return $parent->effective_enable_auto_type;
463 sub effective_enable_searching
{
465 my $enabled = $self->enable_searching;
466 return $enabled if defined $enabled;
468 my $parent = $self->parent or return true
;
469 return $parent->effective_enable_searching;
487 =attr custom_icon_uuid
491 =attr default_auto_type_sequence
493 =attr enable_auto_type
495 =attr enable_searching
497 =attr last_top_visible_entry
501 =attr previous_parent_group
507 =attr last_modification_time
511 =attr last_access_time
519 =attr location_changed
521 Get or set various group fields.