]> Dogcows Code - chaz/p5-File-KDBX/blob - lib/File/KDBX/Group.pm
Further expand support for attributes
[chaz/p5-File-KDBX] / lib / File / KDBX / Group.pm
1 package File::KDBX::Group;
2 # ABSTRACT: A KDBX database group
3
4 use warnings;
5 use strict;
6
7 use Devel::GlobalDestruction;
8 use File::KDBX::Constants qw(:icon);
9 use File::KDBX::Error;
10 use File::KDBX::Util qw(:class :coercion generate_uuid);
11 use Hash::Util::FieldHash;
12 use List::Util qw(sum0);
13 use Ref::Util qw(is_coderef is_ref);
14 use Scalar::Util qw(blessed);
15 use Time::Piece;
16 use boolean;
17 use namespace::clean;
18
19 extends 'File::KDBX::Object';
20
21 our $VERSION = '999.999'; # VERSION
22
23 sub _parent_container { 'groups' }
24
25 # has uuid => sub { generate_uuid(printable => 1) };
26 has name => '', coerce => \&to_string;
27 has notes => '', coerce => \&to_string;
28 has tags => '', coerce => \&to_string;
29 has icon_id => ICON_FOLDER, coerce => \&to_icon_constant;
30 has custom_icon_uuid => undef, coerce => \&to_uuid;
31 has is_expanded => false, coerce => \&to_bool;
32 has default_auto_type_sequence => '', coerce => \&to_string;
33 has enable_auto_type => undef, coerce => \&to_tristate;
34 has enable_searching => undef, coerce => \&to_tristate;
35 has last_top_visible_entry => undef, coerce => \&to_uuid;
36 # has custom_data => {};
37 has previous_parent_group => undef, coerce => \&to_uuid;
38 # has entries => [];
39 # has groups => [];
40 has times => {};
41
42 has last_modification_time => sub { gmtime }, store => 'times', coerce => \&to_time;
43 has creation_time => sub { gmtime }, store => 'times', coerce => \&to_time;
44 has last_access_time => sub { gmtime }, store => 'times', coerce => \&to_time;
45 has expiry_time => sub { gmtime }, store => 'times', coerce => \&to_time;
46 has expires => false, store => 'times', coerce => \&to_bool;
47 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 my $self = shift;
53 $self->$_ for @ATTRS, list_attributes(ref $self);
54 }
55
56 sub uuid {
57 my $self = shift;
58 if (@_ || !defined $self->{uuid}) {
59 my %args = @_ % 2 == 1 ? (uuid => shift, @_) : @_;
60 my $old_uuid = $self->{uuid};
61 my $uuid = $self->{uuid} = delete $args{uuid} // generate_uuid;
62 $self->_signal('uuid.changed', $uuid, $old_uuid) if defined $old_uuid;
63 }
64 $self->{uuid};
65 }
66
67 ##############################################################################
68
69 sub entries {
70 my $self = shift;
71 my $entries = $self->{entries} //= [];
72 # FIXME - Looping through entries on each access is too expensive.
73 @$entries = map { $self->_wrap_entry($_, $self->kdbx) } @$entries;
74 return $entries;
75 }
76
77 sub all_entries {
78 my $self = shift;
79 # FIXME - shouldn't have to delegate to the database to get this
80 return $self->kdbx->all_entries(base => $self);
81 }
82
83 =method add_entry
84
85 $entry = $group->add_entry($entry);
86 $entry = $group->add_entry(%entry_attributes);
87
88 Add an entry to a group. If C<$entry> already has a parent group, it will be removed from that group before
89 being added to C<$group>.
90
91 =cut
92
93 sub add_entry {
94 my $self = shift;
95 my $entry = @_ % 2 == 1 ? shift : undef;
96 my %args = @_;
97
98 my $kdbx = delete $args{kdbx} // eval { $self->kdbx };
99
100 $entry = $self->_wrap_entry($entry // [%args]);
101 $entry->uuid;
102 $entry->kdbx($kdbx) if $kdbx;
103
104 push @{$self->{entries} ||= []}, $entry->remove;
105 return $entry->_set_group($self);
106 }
107
108 sub remove_entry {
109 my $self = shift;
110 my $uuid = is_ref($_[0]) ? $self->_wrap_entry(shift)->uuid : shift;
111 my $objects = $self->{entries};
112 for (my $i = 0; $i < @$objects; ++$i) {
113 my $o = $objects->[$i];
114 next if $uuid ne $o->uuid;
115 return splice @$objects, $i, 1;
116 $o->_set_group(undef);
117 return @$objects, $i, 1;
118 }
119 }
120
121 ##############################################################################
122
123 sub groups {
124 my $self = shift;
125 my $groups = $self->{groups} //= [];
126 # FIXME - Looping through groups on each access is too expensive.
127 @$groups = map { $self->_wrap_group($_, $self->kdbx) } @$groups;
128 return $groups;
129 }
130
131 sub all_groups {
132 my $self = shift;
133 # FIXME - shouldn't have to delegate to the database to get this
134 return $self->kdbx->all_groups(base => $self, include_base => false);
135 }
136
137 sub _kpx_groups { shift->groups(@_) }
138
139 =method add_group
140
141 $new_group = $group->add_group($new_group);
142 $new_group = $group->add_group(%group_attributes);
143
144 Add a group to a group. If C<$new_group> already has a parent group, it will be removed from that group before
145 being added to C<$group>.
146
147 =cut
148
149 sub add_group {
150 my $self = shift;
151 my $group = @_ % 2 == 1 ? shift : undef;
152 my %args = @_;
153
154 my $kdbx = delete $args{kdbx} // eval { $self->kdbx };
155
156 $group = $self->_wrap_group($group // [%args]);
157 $group->uuid;
158 $group->kdbx($kdbx) if $kdbx;
159
160 push @{$self->{groups} ||= []}, $group->remove;
161 return $group->_set_group($self);
162 }
163
164 sub remove_group {
165 my $self = shift;
166 my $uuid = is_ref($_[0]) ? $self->_wrap_group(shift)->uuid : shift;
167 my $objects = $self->{groups};
168 for (my $i = 0; $i < @$objects; ++$i) {
169 my $o = $objects->[$i];
170 next if $uuid ne $o->uuid;
171 $o->_set_group(undef);
172 return splice @$objects, $i, 1;
173 }
174 }
175
176 ##############################################################################
177
178 =method add_object
179
180 $new_entry = $group->add_object($new_entry);
181 $new_group = $group->add_object($new_group);
182
183 Add an object (either a L<File::KDBX::Entry> or a L<File::KDBX::Group>) to a group. This is the generic
184 equivalent of the object forms of L</add_entry> and L</add_group>.
185
186 =cut
187
188 sub add_object {
189 my $self = shift;
190 my $obj = shift;
191 if ($obj->isa('File::KDBX::Entry')) {
192 $self->add_entry($obj);
193 }
194 elsif ($obj->isa('File::KDBX::Group')) {
195 $self->add_group($obj);
196 }
197 }
198
199 =method remove_object
200
201 $group->remove_object($entry);
202 $group->remove_object($group);
203
204 Remove an object (either a L<File::KDBX::Entry> or a L<File::KDBX::Group>) from a group. This is the generic
205 equivalent of the object forms of L</remove_entry> and L</remove_group>.
206
207 =cut
208
209 sub remove_object {
210 my $self = shift;
211 my $object = shift;
212 my $blessed = blessed($object);
213 return $self->remove_group($object, @_) if $blessed && $object->isa('File::KDBX::Group');
214 return $self->remove_entry($object, @_) if $blessed && $object->isa('File::KDBX::Entry');
215 return $self->remove_group($object, @_) || $self->remove_entry($object, @_);
216 }
217
218 ##############################################################################
219
220 =method is_root
221
222 $bool = $group->is_root;
223
224 Determine if a group is the root group of its associated database.
225
226 =cut
227
228 sub is_root {
229 my $self = shift;
230 my $kdbx = eval { $self->kdbx } or return;
231 return Hash::Util::FieldHash::id($kdbx->root) == Hash::Util::FieldHash::id($self);
232 }
233
234 =method path
235
236 $string = $group->path;
237
238 Get a string representation of a group's lineage. This is used as the substitution value for the
239 C<{GROUP_PATH}> placeholder. See L<File::KDBX::Entry/Placeholders>.
240
241 For a root group, the path is simply the name of the group. For deeper groups, the path is a period-separated
242 sequence of group names between the root group and C<$group>, including C<$group> but I<not> the root group.
243 In other words, paths of deeper groups leave the root group name out.
244
245 Database
246 -> Root # path is "Root"
247 -> Foo # path is "Foo"
248 -> Bar # path is "Foo.Bar"
249
250 Yeah, it doesn't make much sense to me, either, but this matches the behavior of KeePass.
251
252 =cut
253
254 sub path {
255 my $self = shift;
256 return $self->name if $self->is_root;
257 my $lineage = $self->lineage or return;
258 my @parts = (@$lineage, $self);
259 shift @parts;
260 return join('.', map { $_->name } @parts);
261 }
262
263 =method size
264
265 $size = $group->size;
266
267 Get the size (in bytes) of a group, including the size of all subroups and entries, if any.
268
269 =cut
270
271 sub size {
272 my $self = shift;
273 return sum0 map { $_->size } @{$self->groups}, @{$self->entries};
274 }
275
276 =method depth
277
278 $depth = $group->depth;
279
280 Get the depth of a group within a database. The root group is at depth 0, its direct children are at depth 1,
281 etc. A group not in a database tree structure returns a depth of -1.
282
283 =cut
284
285 sub depth { $_[0]->is_root ? 0 : (scalar @{$_[0]->lineage || []} || -1) }
286
287 sub label { shift->name(@_) }
288
289 sub _signal {
290 my $self = shift;
291 my $type = shift;
292 return $self->SUPER::_signal("group.$type", @_);
293 }
294
295 sub _commit {
296 my $self = shift;
297 my $time = gmtime;
298 $self->last_modification_time($time);
299 $self->last_access_time($time);
300 }
301
302 1;
303 __END__
304
305 =head1 DESCRIPTION
306
307 =attr uuid
308
309 =attr name
310
311 =attr notes
312
313 =attr tags
314
315 =attr icon_id
316
317 =attr custom_icon_uuid
318
319 =attr is_expanded
320
321 =attr default_auto_type_sequence
322
323 =attr enable_auto_type
324
325 =attr enable_searching
326
327 =attr last_top_visible_entry
328
329 =attr custom_data
330
331 =attr previous_parent_group
332
333 =attr entries
334
335 =attr groups
336
337 =attr last_modification_time
338
339 =attr creation_time
340
341 =attr last_access_time
342
343 =attr expiry_time
344
345 =attr expires
346
347 =attr usage_count
348
349 =attr location_changed
350
351 Get or set various group fields.
352
353 =cut
This page took 0.055693 seconds and 4 git commands to generate.