1 package File
::KDBX
::Group
;
2 # ABSTRACT: A KDBX database group
7 use Devel
::GlobalDestruction
;
8 use File
::KDBX
::Constants
qw(:bool :icon :iteration);
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
26 The human-readable name of the group
.
30 Free form text string associated with the group
.
34 Whether
or not subgroups are visible
when listed
for user selection
.
36 =attr default_auto_type_sequence
38 The
default auto-type keystroke sequence
, inheritable by entries
and subgroups
.
40 =attr enable_auto_type
42 Whether
or not the entry
is eligible to be matched
for auto-typing
, inheritable by entries
and subgroups
.
44 =attr enable_searching
46 Whether
or not entries within the group can show up
in search results
, inheritable by subgroups
.
48 =attr last_top_visible_entry
50 The UUID of the entry visible at the top of the list
.
54 Array of entries contained within the group
.
58 Array of subgroups contained within the group
.
62 # has uuid => sub { generate_uuid(printable => 1) };
63 has name
=> '', coerce
=> \
&to_string
;
64 has notes
=> '', coerce
=> \
&to_string
;
65 has tags
=> '', coerce
=> \
&to_string
;
66 has icon_id
=> ICON_FOLDER
, coerce
=> \
&to_icon_constant
;
67 has custom_icon_uuid
=> undef, coerce
=> \
&to_uuid
;
68 has is_expanded
=> false
, coerce
=> \
&to_bool
;
69 has default_auto_type_sequence
=> '', coerce
=> \
&to_string
;
70 has enable_auto_type
=> undef, coerce
=> \
&to_tristate
;
71 has enable_searching
=> undef, coerce
=> \
&to_tristate
;
72 has last_top_visible_entry
=> undef, coerce
=> \
&to_uuid
;
73 # has custom_data => {};
74 has previous_parent_group
=> undef, coerce
=> \
&to_uuid
;
79 has last_modification_time
=> sub { gmtime }, store
=> 'times', coerce
=> \
&to_time
;
80 has creation_time
=> sub { gmtime }, store
=> 'times', coerce
=> \
&to_time
;
81 has last_access_time
=> sub { gmtime }, store
=> 'times', coerce
=> \
&to_time
;
82 has expiry_time
=> sub { gmtime }, store
=> 'times', coerce
=> \
&to_time
;
83 has expires
=> false
, store
=> 'times', coerce
=> \
&to_bool
;
84 has usage_count
=> 0, store
=> 'times', coerce
=> \
&to_number
;
85 has location_changed
=> sub { gmtime }, store
=> 'times', coerce
=> \
&to_time
;
87 my @ATTRS = qw(uuid custom_data entries groups);
88 sub _set_nonlazy_attributes
{
90 $self->$_ for @ATTRS, list_attributes
(ref $self);
95 if (@_ || !defined $self->{uuid
}) {
96 my %args = @_ % 2 == 1 ? (uuid
=> shift, @_) : @_;
97 my $old_uuid = $self->{uuid
};
98 my $uuid = $self->{uuid
} = delete $args{uuid
} // generate_uuid
;
99 $self->_signal('uuid.changed', $uuid, $old_uuid) if defined $old_uuid;
104 ##############################################################################
108 \
@entries = $group->entries;
110 Get an array of direct child entries within a group
.
116 my $entries = $self->{entries
} //= [];
117 if (@$entries && !blessed
($entries->[0])) {
118 @$entries = map { $self->_wrap_entry($_, $self->kdbx) } @$entries;
120 assert
{ !any
{ !blessed
$_ } @$entries };
126 \
&iterator
= $kdbx->all_entries(%options);
128 Get an L
<File
::KDBX
::Iterator
> over I
<entries
> within a group
. Supports the same options as L
</groups
>,
132 * C<auto_type> - Only include entries with auto-type enabled (default: false, include all)
133 * C<searching> - Only include entries within groups with searching enabled (default: false, include all)
134 * C<history> - Also include historical entries (default: false, include only current entries)
142 my $searching = delete $args{searching
};
143 my $auto_type = delete $args{auto_type
};
144 my $history = delete $args{history
};
146 my $groups = $self->all_groups(%args);
149 return File
::KDBX
::Iterator-
>new(sub {
151 while (my $group = $groups->next) {
152 next if $searching && !$group->effective_enable_searching;
153 next if $auto_type && !$group->effective_enable_auto_type;
154 @entries = @{$group->entries};
155 @entries = grep { $_->auto_type->{enabled
} } @entries if $auto_type;
156 @entries = map { ($_, @{$_->history}) } @entries if $history;
166 $entry = $group->add_entry($entry);
167 $entry = $group->add_entry(%entry_attributes);
169 Add an entry to a group
. If C
<$entry> already
has a parent group
, it will be removed from that group before
170 being added to C
<$group>.
176 my $entry = @_ % 2 == 1 ? shift : undef;
179 my $kdbx = delete $args{kdbx
} // eval { $self->kdbx };
181 $entry = $self->_wrap_entry($entry // [%args]);
183 $entry->kdbx($kdbx) if $kdbx;
185 push @{$self->{entries
} ||= []}, $entry->remove;
186 return $entry->_set_group($self)->_signal('added', $self);
191 $entry = $group->remove_entry($entry);
192 $entry = $group->remove_entry($entry_uuid);
194 Remove an entry from a group
's array of entries. Returns the entry removed or C<undef> if nothing removed.
200 my $uuid = is_ref($_[0]) ? $self->_wrap_entry(shift)->uuid : shift;
202 my $objects = $self->{entries};
203 for (my $i = 0; $i < @$objects; ++$i) {
204 my $object = $objects->[$i];
205 next if $uuid ne $object->uuid;
206 $object->_set_group(undef);
207 $object->_signal('removed
') if $args{signal} // 1;
208 return splice @$objects, $i, 1;
212 ##############################################################################
216 \@groups = $group->groups;
218 Get an array of direct subgroups within a group.
224 my $groups = $self->{groups} //= [];
225 if (@$groups && !blessed($groups->[0])) {
226 @$groups = map { $self->_wrap_group($_, $self->kdbx) } @$groups;
228 assert { !any { !blessed $_ } @$groups };
234 \&iterator = $group->all_groups(%options);
236 Get an L<File::KDBX::Iterator> over I<groups> within a groups, deeply. Options:
239 * C<inclusive> - Include C<$group> itself in the results (default: true)
240 * C<algorithm> - Search algorithm, one of C<ids>, C<bfs> or C<dfs> (default: C<ids>)
248 my @groups = ($args{inclusive} // 1) ? $self : @{$self->groups};
249 my $algo = lc($args{algorithm} || 'ids
');
251 if ($algo eq ITERATION_DFS) {
253 return File::KDBX::Iterator->new(sub {
254 my $next = shift @groups or return;
255 if (!$visited{Hash::Util::FieldHash::id($next)}++) {
256 while (my @children = @{$next->groups}) {
257 unshift @groups, @children, $next;
258 $next = shift @groups;
259 $visited{Hash::Util::FieldHash::id($next)}++;
265 elsif ($algo eq ITERATION_BFS) {
266 return File::KDBX::Iterator->new(sub {
267 my $next = shift @groups or return;
268 push @groups, @{$next->groups};
272 return File::KDBX::Iterator->new(sub {
273 my $next = shift @groups or return;
274 unshift @groups, @{$next->groups};
279 sub _kpx_groups { shift->groups(@_) }
283 $new_group = $group->add_group($new_group);
284 $new_group = $group->add_group(%group_attributes);
286 Add a group to a group. If C<$new_group> already has a parent group, it will be removed from that group before
287 being added to C<$group>.
293 my $group = @_ % 2 == 1 ? shift : undef;
296 my $kdbx = delete $args{kdbx} // eval { $self->kdbx };
298 $group = $self->_wrap_group($group // [%args]);
300 $group->kdbx($kdbx) if $kdbx;
302 push @{$self->{groups} ||= []}, $group->remove;
303 return $group->_set_group($self)->_signal('added
', $self);
308 $removed_group = $group->remove_group($group);
309 $removed_group = $group->remove_group($group_uuid);
311 Remove a group from a group's array of subgroups
. Returns the group removed
or C
<undef> if nothing removed
.
317 my $uuid = is_ref
($_[0]) ? $self->_wrap_group(shift)->uuid : shift;
319 my $objects = $self->{groups
};
320 for (my $i = 0; $i < @$objects; ++$i) {
321 my $object = $objects->[$i];
322 next if $uuid ne $object->uuid;
323 $object->_set_group(undef);
324 $object->_signal('removed') if $args{signal
} // 1;
325 return splice @$objects, $i, 1;
329 ##############################################################################
333 \
&iterator
= $groups->all_objects(%options);
335 Get an L
<File
::KDBX
::Iterator
> over I
<objects
> within a group
, deeply
. Groups
and entries are considered
336 objects
, so this
is essentially a combination of L
</groups> and L</entries
>. This won
't often be useful, but
337 it can be convenient for maintenance tasks. This method takes the same options as L</groups> and L</entries>.
345 my $searching = delete $args{searching};
346 my $auto_type = delete $args{auto_type};
347 my $history = delete $args{history};
349 my $groups = $self->all_groups(%args);
352 return File::KDBX::Iterator->new(sub {
354 while (my $group = $groups->next) {
355 next if $searching && !$group->effective_enable_searching;
356 next if $auto_type && !$group->effective_enable_auto_type;
357 @entries = @{$group->entries};
358 @entries = grep { $_->auto_type->{enabled} } @entries if $auto_type;
359 @entries = map { ($_, @{$_->history}) } @entries if $history;
369 $new_entry = $group->add_object($new_entry);
370 $new_group = $group->add_object($new_group);
372 Add an object (either a L<File::KDBX::Entry> or a L<File::KDBX::Group>) to a group. This is the generic
373 equivalent of the object forms of L</add_entry> and L</add_group>.
380 if ($obj->isa('File
::KDBX
::Entry
')) {
381 $self->add_entry($obj);
383 elsif ($obj->isa('File
::KDBX
::Group
')) {
384 $self->add_group($obj);
388 =method remove_object
390 $group->remove_object($entry);
391 $group->remove_object($group);
393 Remove an object (either a L<File::KDBX::Entry> or a L<File::KDBX::Group>) from a group. This is the generic
394 equivalent of the object forms of L</remove_entry> and L</remove_group>.
401 my $blessed = blessed($object);
402 return $self->remove_group($object, @_) if $blessed && $object->isa('File
::KDBX
::Group
');
403 return $self->remove_entry($object, @_) if $blessed && $object->isa('File
::KDBX
::Entry
');
404 return $self->remove_group($object, @_) || $self->remove_entry($object, @_);
407 ##############################################################################
409 =method effective_default_auto_type_sequence
411 $text = $group->effective_default_auto_type_sequence;
413 Get the value of L</default_auto_type_sequence>, if set, or get the inherited effective default auto-type
414 sequence of the parent.
418 sub effective_default_auto_type_sequence {
420 my $sequence = $self->default_auto_type_sequence;
421 return $sequence if defined $sequence;
423 my $parent = $self->group or return '{USERNAME
}{TAB
}{PASSWORD
}{ENTER
}';
424 return $parent->effective_default_auto_type_sequence;
427 =method effective_enable_auto_type
429 $text = $group->effective_enable_auto_type;
431 Get the value of L</enable_auto_type>, if set, or get the inherited effective auto-type enabled value of the
436 sub effective_enable_auto_type {
438 my $enabled = $self->enable_auto_type;
439 return $enabled if defined $enabled;
441 my $parent = $self->group or return true;
442 return $parent->effective_enable_auto_type;
445 =method effective_enable_searching
447 $text = $group->effective_enable_searching;
449 Get the value of L</enable_searching>, if set, or get the inherited effective searching enabled value of the
454 sub effective_enable_searching {
456 my $enabled = $self->enable_searching;
457 return $enabled if defined $enabled;
459 my $parent = $self->group or return true;
460 return $parent->effective_enable_searching;
463 ##############################################################################
467 $bool = $group->is_empty;
469 Get whether or not the group is empty (has no subgroups or entries).
475 return @{$self->groups} == 0 && @{$self->entries} == 0;
480 $bool = $group->is_root;
482 Determine if a group is the root group of its connected database.
488 my $kdbx = eval { $self->kdbx } or return FALSE;
489 return Hash::Util::FieldHash::id($kdbx->root) == Hash::Util::FieldHash::id($self);
492 =method is_recycle_bin
494 $bool = $group->is_recycle_bin;
496 Get whether or not a group is the recycle bin of its connected database.
502 my $kdbx = eval { $self->kdbx } or return FALSE;
503 my $group = $kdbx->recycle_bin;
504 return $group && Hash::Util::FieldHash::id($group) == Hash::Util::FieldHash::id($self);
507 =method is_entry_templates
509 $bool = $group->is_entry_templates;
511 Get whether or not a group is the group containing entry template in its connected database.
515 sub is_entry_templates {
517 my $kdbx = eval { $self->kdbx } or return FALSE;
518 my $group = $kdbx->entry_templates;
519 return $group && Hash::Util::FieldHash::id($group) == Hash::Util::FieldHash::id($self);
522 =method is_last_selected
524 $bool = $group->is_last_selected;
526 Get whether or not a group is the prior selected group of its connected database.
530 sub is_last_selected {
532 my $kdbx = eval { $self->kdbx } or return FALSE;
533 my $group = $kdbx->last_selected;
534 return $group && Hash::Util::FieldHash::id($group) == Hash::Util::FieldHash::id($self);
537 =method is_last_top_visible
539 $bool = $group->is_last_top_visible;
541 Get whether or not a group is the latest top visible group of its connected database.
545 sub is_last_top_visible {
547 my $kdbx = eval { $self->kdbx } or return FALSE;
548 my $group = $kdbx->last_top_visible;
549 return $group && Hash::Util::FieldHash::id($group) == Hash::Util::FieldHash::id($self);
554 $string = $group->path;
556 Get a string representation of a group's lineage
. This
is used as the substitution value
for the
557 C
<{GROUP_PATH
}> placeholder
. See L
<File
::KDBX
::Entry
/Placeholders
>.
559 For a root group
, the path
is simply the name of the group
. For deeper groups
, the path
is a period-separated
560 sequence of group names between the root group
and C
<$group>, including C
<$group> but I
<not> the root group
.
561 In other words
, paths of deeper groups leave the root group name out
.
564 -> Root
# path is "Root"
565 -> Foo
# path is "Foo"
566 -> Bar
# path is "Foo.Bar"
568 Yeah
, it doesn
't make much sense to me, either, but this matches the behavior of KeePass.
574 return $self->name if $self->is_root;
575 my $lineage = $self->lineage or return;
576 my @parts = (@$lineage, $self);
578 return join('.', map { $_->name } @parts);
583 $size = $group->size;
585 Get the size (in bytes) of a group, including the size of all subroups and entries, if any.
591 return sum0 map { $_->size } @{$self->groups}, @{$self->entries};
596 $depth = $group->depth;
598 Get the depth of a group within a database. The root group is at depth 0, its direct children are at depth 1,
599 etc. A group not in a database tree structure returns a depth of -1.
603 sub depth { $_[0]->is_root ? 0 : (scalar @{$_[0]->lineage || []} || -1) }
608 return $self->SUPER::_signal("group.$type", @_);
614 $self->last_modification_time($time);
615 $self->last_access_time($time);
618 sub label { shift->name(@_) }
620 ### Name of the parent attribute expected to contain the object
621 sub _parent_container { 'groups
' }
626 =for Pod::Coverage times
630 A group in a KDBX database is a type of object that can contain entries and other groups.
632 There is also some metadata associated with a group. Each group in a database is identified uniquely by
633 a UUID. An entry can also have an icon associated with it, and there are various timestamps. Take a look at
634 the attributes to see what's available
.
636 A B
<File
::KDBX
::Group
> is a subclass of L
<File
::KDBX
::Object
>. View its documentation to see other attributes
637 and methods available on groups
.