| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package File::KDBX::Object; |
|
2
|
|
|
|
|
|
|
# ABSTRACT: A KDBX database object |
|
3
|
|
|
|
|
|
|
|
|
4
|
11
|
|
|
11
|
|
6719
|
use warnings; |
|
|
11
|
|
|
|
|
23
|
|
|
|
11
|
|
|
|
|
356
|
|
|
5
|
11
|
|
|
11
|
|
51
|
use strict; |
|
|
11
|
|
|
|
|
19
|
|
|
|
11
|
|
|
|
|
214
|
|
|
6
|
|
|
|
|
|
|
|
|
7
|
11
|
|
|
11
|
|
46
|
use Devel::GlobalDestruction; |
|
|
11
|
|
|
|
|
16
|
|
|
|
11
|
|
|
|
|
61
|
|
|
8
|
11
|
|
|
11
|
|
709
|
use File::KDBX::Constants qw(:bool); |
|
|
11
|
|
|
|
|
17
|
|
|
|
11
|
|
|
|
|
1035
|
|
|
9
|
11
|
|
|
11
|
|
60
|
use File::KDBX::Error; |
|
|
11
|
|
|
|
|
21
|
|
|
|
11
|
|
|
|
|
416
|
|
|
10
|
11
|
|
|
11
|
|
60
|
use File::KDBX::Util qw(:uuid); |
|
|
11
|
|
|
|
|
20
|
|
|
|
11
|
|
|
|
|
985
|
|
|
11
|
11
|
|
|
11
|
|
60
|
use Hash::Util::FieldHash qw(fieldhashes); |
|
|
11
|
|
|
|
|
21
|
|
|
|
11
|
|
|
|
|
440
|
|
|
12
|
11
|
|
|
11
|
|
56
|
use List::Util qw(any first); |
|
|
11
|
|
|
|
|
18
|
|
|
|
11
|
|
|
|
|
600
|
|
|
13
|
11
|
|
|
11
|
|
56
|
use Ref::Util qw(is_arrayref is_plain_arrayref is_plain_hashref is_ref); |
|
|
11
|
|
|
|
|
17
|
|
|
|
11
|
|
|
|
|
507
|
|
|
14
|
11
|
|
|
11
|
|
63
|
use Scalar::Util qw(blessed weaken); |
|
|
11
|
|
|
|
|
22
|
|
|
|
11
|
|
|
|
|
448
|
|
|
15
|
11
|
|
|
11
|
|
62
|
use namespace::clean; |
|
|
11
|
|
|
|
|
18
|
|
|
|
11
|
|
|
|
|
56
|
|
|
16
|
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
our $VERSION = '0.905'; # VERSION |
|
18
|
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
fieldhashes \my (%KDBX, %PARENT, %TXNS, %REFS, %SIGNALS); |
|
20
|
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
sub new { |
|
23
|
237
|
|
|
237
|
1
|
99224
|
my $class = shift; |
|
24
|
|
|
|
|
|
|
|
|
25
|
|
|
|
|
|
|
# copy constructor |
|
26
|
237
|
50
|
100
|
|
|
705
|
return $_[0]->clone if @_ == 1 && blessed $_[0] && $_[0]->isa($class); |
|
|
|
|
66
|
|
|
|
|
|
27
|
|
|
|
|
|
|
|
|
28
|
237
|
|
|
|
|
319
|
my $data; |
|
29
|
237
|
100
|
|
|
|
680
|
$data = shift if is_plain_hashref($_[0]); |
|
30
|
|
|
|
|
|
|
|
|
31
|
237
|
|
|
|
|
301
|
my $kdbx; |
|
32
|
237
|
100
|
|
|
|
526
|
$kdbx = shift if @_ % 2 == 1; |
|
33
|
|
|
|
|
|
|
|
|
34
|
237
|
|
|
|
|
573
|
my %args = @_; |
|
35
|
237
|
100
|
33
|
|
|
757
|
$args{kdbx} //= $kdbx if defined $kdbx; |
|
36
|
|
|
|
|
|
|
|
|
37
|
237
|
|
100
|
|
|
784
|
my $self = bless $data // {}, $class; |
|
38
|
237
|
|
|
|
|
891
|
$self->init(%args); |
|
39
|
237
|
100
|
|
|
|
754
|
$self->_set_nonlazy_attributes if !$data; |
|
40
|
237
|
|
|
|
|
5608
|
return $self; |
|
41
|
|
|
|
|
|
|
} |
|
42
|
|
|
|
|
|
|
|
|
43
|
0
|
|
|
0
|
|
0
|
sub _set_nonlazy_attributes { die 'Not implemented' } |
|
44
|
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
sub init { |
|
47
|
133
|
|
|
133
|
1
|
184
|
my $self = shift; |
|
48
|
133
|
|
|
|
|
311
|
my %args = @_; |
|
49
|
|
|
|
|
|
|
|
|
50
|
133
|
|
|
|
|
382
|
while (my ($key, $val) = each %args) { |
|
51
|
331
|
50
|
|
|
|
1156
|
if (my $method = $self->can($key)) { |
|
52
|
331
|
|
|
|
|
652
|
$self->$method($val); |
|
53
|
|
|
|
|
|
|
} |
|
54
|
|
|
|
|
|
|
} |
|
55
|
|
|
|
|
|
|
|
|
56
|
133
|
|
|
|
|
309
|
return $self; |
|
57
|
|
|
|
|
|
|
} |
|
58
|
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
sub wrap { |
|
61
|
798
|
|
|
798
|
1
|
1078
|
my $class = shift; |
|
62
|
798
|
|
|
|
|
888
|
my $object = shift; |
|
63
|
798
|
100
|
66
|
|
|
4817
|
return $object if blessed $object && $object->isa($class); |
|
64
|
114
|
100
|
|
|
|
293
|
return $class->new(@_, @$object) if is_arrayref($object); |
|
65
|
72
|
|
|
|
|
188
|
return $class->new($object, @_); |
|
66
|
|
|
|
|
|
|
} |
|
67
|
|
|
|
|
|
|
|
|
68
|
|
|
|
|
|
|
|
|
69
|
0
|
|
|
0
|
1
|
0
|
sub label { die 'Not implemented' } |
|
70
|
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
my %CLONE = (entries => 1, groups => 1, history => 1); |
|
73
|
|
|
|
|
|
|
sub clone { |
|
74
|
31
|
|
|
31
|
1
|
54
|
my $self = shift; |
|
75
|
31
|
|
|
|
|
79
|
my %args = @_; |
|
76
|
|
|
|
|
|
|
|
|
77
|
31
|
|
100
|
|
|
141
|
local $CLONE{new_uuid} = $args{new_uuid} // $args{parent} // 0; |
|
|
|
|
100
|
|
|
|
|
|
78
|
31
|
|
100
|
|
|
101
|
local $CLONE{entries} = $args{entries} // 1; |
|
79
|
31
|
|
100
|
|
|
72
|
local $CLONE{groups} = $args{groups} // 1; |
|
80
|
31
|
|
100
|
|
|
61
|
local $CLONE{history} = $args{history} // 1; |
|
81
|
31
|
|
50
|
|
|
81
|
local $CLONE{reference_password} = $args{reference_password} // 0; |
|
82
|
31
|
|
100
|
|
|
84
|
local $CLONE{reference_username} = $args{reference_username} // 0; |
|
83
|
|
|
|
|
|
|
|
|
84
|
31
|
|
|
|
|
122
|
require Storable; |
|
85
|
31
|
|
|
|
|
437
|
my $copy = Storable::dclone($self); |
|
86
|
|
|
|
|
|
|
|
|
87
|
31
|
100
|
66
|
|
|
87
|
if ($args{relabel} and my $label = $self->label) { |
|
88
|
3
|
|
|
|
|
11
|
$copy->label("$label - Copy"); |
|
89
|
|
|
|
|
|
|
} |
|
90
|
31
|
100
|
66
|
|
|
69
|
if ($args{parent} and my $parent = $self->group) { |
|
91
|
3
|
|
|
|
|
8
|
$parent->add_object($copy); |
|
92
|
|
|
|
|
|
|
} |
|
93
|
|
|
|
|
|
|
|
|
94
|
31
|
|
|
|
|
304
|
return $copy; |
|
95
|
|
|
|
|
|
|
} |
|
96
|
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
sub STORABLE_freeze { |
|
98
|
64
|
|
|
64
|
0
|
113
|
my $self = shift; |
|
99
|
64
|
|
|
|
|
73
|
my $cloning = shift; |
|
100
|
|
|
|
|
|
|
|
|
101
|
64
|
|
|
|
|
565
|
my $copy = {%$self}; |
|
102
|
64
|
100
|
|
|
|
172
|
delete $copy->{entries} if !$CLONE{entries}; |
|
103
|
64
|
100
|
|
|
|
107
|
delete $copy->{groups} if !$CLONE{groups}; |
|
104
|
64
|
100
|
|
|
|
110
|
delete $copy->{history} if !$CLONE{history}; |
|
105
|
|
|
|
|
|
|
|
|
106
|
64
|
50
|
|
|
|
3959
|
return ($cloning ? Hash::Util::FieldHash::id($self) : ''), $copy; |
|
107
|
|
|
|
|
|
|
} |
|
108
|
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
sub STORABLE_thaw { |
|
110
|
64
|
|
|
64
|
0
|
133
|
my $self = shift; |
|
111
|
64
|
|
|
|
|
79
|
my $cloning = shift; |
|
112
|
64
|
|
|
|
|
75
|
my $addr = shift; |
|
113
|
64
|
|
|
|
|
73
|
my $copy = shift; |
|
114
|
|
|
|
|
|
|
|
|
115
|
64
|
|
|
|
|
479
|
@$self{keys %$copy} = values %$copy; |
|
116
|
|
|
|
|
|
|
|
|
117
|
64
|
50
|
|
|
|
180
|
if ($cloning) { |
|
118
|
64
|
|
|
|
|
121
|
my $kdbx = $KDBX{$addr}; |
|
119
|
64
|
100
|
|
|
|
142
|
$self->kdbx($kdbx) if $kdbx; |
|
120
|
|
|
|
|
|
|
} |
|
121
|
|
|
|
|
|
|
|
|
122
|
64
|
50
|
|
|
|
125
|
if (defined $self->{uuid}) { |
|
123
|
64
|
100
|
66
|
|
|
226
|
if (($CLONE{reference_password} || $CLONE{reference_username}) && $self->can('strings')) { |
|
|
|
|
66
|
|
|
|
|
|
124
|
2
|
|
|
|
|
6
|
my $uuid = format_uuid($self->{uuid}); |
|
125
|
2
|
|
|
|
|
3
|
my $clone_obj = do { |
|
126
|
2
|
|
|
|
|
5
|
local $CLONE{new_uuid} = 0; |
|
127
|
2
|
|
|
|
|
3
|
local $CLONE{entries} = 1; |
|
128
|
2
|
|
|
|
|
3
|
local $CLONE{groups} = 1; |
|
129
|
2
|
|
|
|
|
5
|
local $CLONE{history} = 1; |
|
130
|
2
|
|
|
|
|
4
|
local $CLONE{reference_password} = 0; |
|
131
|
2
|
|
|
|
|
2
|
local $CLONE{reference_username} = 0; |
|
132
|
|
|
|
|
|
|
# Clone only the entry's data and manually bless to avoid infinite recursion. |
|
133
|
2
|
|
|
|
|
107
|
bless Storable::dclone({%$copy}), 'File::KDBX::Entry'; |
|
134
|
|
|
|
|
|
|
}; |
|
135
|
2
|
|
|
|
|
8
|
my $txn = $self->begin_work(snapshot => $clone_obj); |
|
136
|
2
|
50
|
|
|
|
7
|
if ($CLONE{reference_password}) { |
|
137
|
0
|
|
|
|
|
0
|
$self->password("{REF:P\@I:$uuid}"); |
|
138
|
|
|
|
|
|
|
} |
|
139
|
2
|
50
|
|
|
|
5
|
if ($CLONE{reference_username}) { |
|
140
|
2
|
|
|
|
|
8
|
$self->username("{REF:U\@I:$uuid}"); |
|
141
|
|
|
|
|
|
|
} |
|
142
|
2
|
|
|
|
|
5
|
$txn->commit; |
|
143
|
|
|
|
|
|
|
} |
|
144
|
64
|
100
|
|
|
|
139
|
$self->uuid(generate_uuid) if $CLONE{new_uuid}; |
|
145
|
|
|
|
|
|
|
} |
|
146
|
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
# Dualvars aren't cloned as dualvars, so dualify the icon. |
|
148
|
64
|
50
|
|
|
|
199
|
$self->icon_id($self->{icon_id}) if defined $self->{icon_id}; |
|
149
|
|
|
|
|
|
|
} |
|
150
|
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
sub kdbx { |
|
153
|
978
|
|
|
978
|
1
|
4826
|
my $self = shift; |
|
154
|
978
|
50
|
|
|
|
1696
|
$self = $self->new if !ref $self; |
|
155
|
978
|
100
|
|
|
|
1601
|
if (@_) { |
|
156
|
310
|
50
|
|
|
|
506
|
if (my $kdbx = shift) { |
|
157
|
310
|
|
|
|
|
1632
|
$KDBX{$self} = $kdbx; |
|
158
|
310
|
|
|
|
|
819
|
weaken $KDBX{$self}; |
|
159
|
|
|
|
|
|
|
} |
|
160
|
|
|
|
|
|
|
else { |
|
161
|
0
|
|
|
|
|
0
|
delete $KDBX{$self}; |
|
162
|
|
|
|
|
|
|
} |
|
163
|
|
|
|
|
|
|
} |
|
164
|
978
|
100
|
|
|
|
3421
|
$KDBX{$self} or throw 'Object is disconnected', object => $self; |
|
165
|
|
|
|
|
|
|
} |
|
166
|
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
sub is_connected { |
|
169
|
84
|
|
|
84
|
1
|
133
|
my $self = shift; |
|
170
|
84
|
|
|
|
|
126
|
return !!eval { $self->kdbx }; |
|
|
84
|
|
|
|
|
159
|
|
|
171
|
|
|
|
|
|
|
} |
|
172
|
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
|
|
174
|
5
|
|
|
5
|
1
|
44
|
sub id { format_uuid(shift->uuid, @_) } |
|
175
|
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
|
|
177
|
|
|
|
|
|
|
sub group { |
|
178
|
154
|
|
|
154
|
1
|
191
|
my $self = shift; |
|
179
|
|
|
|
|
|
|
|
|
180
|
154
|
100
|
|
|
|
293
|
if (my $new_group = shift) { |
|
181
|
1
|
|
|
|
|
5
|
my $old_group = $self->group; |
|
182
|
1
|
50
|
|
|
|
6
|
return $new_group if Hash::Util::FieldHash::id($old_group) == Hash::Util::FieldHash::id($new_group); |
|
183
|
|
|
|
|
|
|
# move to a new parent |
|
184
|
1
|
50
|
|
|
|
4
|
$self->remove(signal => 0) if $old_group; |
|
185
|
1
|
|
|
|
|
4
|
$self->location_changed('now'); |
|
186
|
1
|
|
|
|
|
17
|
$new_group->add_object($self); |
|
187
|
|
|
|
|
|
|
} |
|
188
|
|
|
|
|
|
|
|
|
189
|
154
|
|
|
|
|
275
|
my $id = Hash::Util::FieldHash::id($self); |
|
190
|
154
|
100
|
|
|
|
316
|
if (my $group = $PARENT{$self}) { |
|
191
|
31
|
|
|
|
|
64
|
my $method = $self->_parent_container; |
|
192
|
31
|
50
|
|
31
|
|
81
|
return $group if first { $id == Hash::Util::FieldHash::id($_) } @{$group->$method}; |
|
|
31
|
|
|
|
|
150
|
|
|
|
31
|
|
|
|
|
76
|
|
|
193
|
0
|
|
|
|
|
0
|
delete $PARENT{$self}; |
|
194
|
|
|
|
|
|
|
} |
|
195
|
|
|
|
|
|
|
# always get lineage from root to leaf because the other way requires parent, so it would be recursive |
|
196
|
123
|
100
|
|
|
|
207
|
my $lineage = $self->kdbx->_trace_lineage($self) or return; |
|
197
|
1
|
50
|
|
|
|
6
|
my $group = pop @$lineage or return; |
|
198
|
1
|
|
|
|
|
4
|
$PARENT{$self} = $group; weaken $PARENT{$self}; |
|
|
1
|
|
|
|
|
3
|
|
|
199
|
1
|
|
|
|
|
2
|
return $group; |
|
200
|
|
|
|
|
|
|
} |
|
201
|
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
sub _set_group { |
|
203
|
110
|
|
|
110
|
|
135
|
my $self = shift; |
|
204
|
110
|
100
|
|
|
|
173
|
if (my $parent = shift) { |
|
205
|
48
|
|
|
|
|
121
|
$PARENT{$self} = $parent; |
|
206
|
48
|
|
|
|
|
116
|
weaken $PARENT{$self}; |
|
207
|
|
|
|
|
|
|
} |
|
208
|
|
|
|
|
|
|
else { |
|
209
|
62
|
|
|
|
|
171
|
delete $PARENT{$self}; |
|
210
|
|
|
|
|
|
|
} |
|
211
|
110
|
|
|
|
|
211
|
return $self; |
|
212
|
|
|
|
|
|
|
} |
|
213
|
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
### Name of the parent attribute expected to contain the object |
|
215
|
0
|
|
|
0
|
|
0
|
sub _parent_container { die 'Not implemented' } |
|
216
|
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
sub lineage { |
|
219
|
5
|
|
|
5
|
1
|
8
|
my $self = shift; |
|
220
|
5
|
|
|
|
|
6
|
my $base = shift; |
|
221
|
|
|
|
|
|
|
|
|
222
|
5
|
50
|
|
|
|
11
|
my $base_addr = $base ? Hash::Util::FieldHash::id($base) : 0; |
|
223
|
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
# try leaf to root |
|
225
|
5
|
|
|
|
|
6
|
my @path; |
|
226
|
5
|
|
|
|
|
7
|
my $object = $self; |
|
227
|
5
|
|
|
|
|
10
|
while ($object = $object->group) { |
|
228
|
8
|
|
|
|
|
13
|
unshift @path, $object; |
|
229
|
8
|
50
|
|
|
|
22
|
last if $base_addr == Hash::Util::FieldHash::id($object); |
|
230
|
|
|
|
|
|
|
} |
|
231
|
5
|
50
|
33
|
|
|
29
|
return \@path if @path && ($base_addr == Hash::Util::FieldHash::id($path[0]) || $path[0]->is_root); |
|
|
|
|
33
|
|
|
|
|
|
232
|
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
# try root to leaf |
|
234
|
0
|
|
|
|
|
0
|
return $self->kdbx->_trace_lineage($self, $base); |
|
235
|
|
|
|
|
|
|
} |
|
236
|
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
sub remove { |
|
239
|
55
|
|
|
55
|
1
|
88
|
my $self = shift; |
|
240
|
55
|
|
|
|
|
104
|
my $parent = $self->group; |
|
241
|
55
|
100
|
|
|
|
122
|
$parent->remove_object($self, @_) if $parent; |
|
242
|
55
|
|
|
|
|
163
|
$self->_set_group(undef); |
|
243
|
55
|
|
|
|
|
121
|
return $self; |
|
244
|
|
|
|
|
|
|
} |
|
245
|
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
|
|
247
|
|
|
|
|
|
|
sub recycle { |
|
248
|
1
|
|
|
1
|
1
|
2
|
my $self = shift; |
|
249
|
1
|
|
|
|
|
3
|
return $self->group($self->kdbx->recycle_bin); |
|
250
|
|
|
|
|
|
|
} |
|
251
|
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
sub recycle_or_remove { |
|
254
|
3
|
|
|
3
|
1
|
19
|
my $self = shift; |
|
255
|
3
|
|
|
|
|
5
|
my $kdbx = eval { $self->kdbx }; |
|
|
3
|
|
|
|
|
6
|
|
|
256
|
3
|
100
|
66
|
|
|
13
|
if ($kdbx && $kdbx->recycle_bin_enabled && !$self->is_recycled) { |
|
|
|
|
100
|
|
|
|
|
|
257
|
1
|
|
|
|
|
5
|
$self->recycle; |
|
258
|
|
|
|
|
|
|
} |
|
259
|
|
|
|
|
|
|
else { |
|
260
|
2
|
|
|
|
|
21
|
$self->remove; |
|
261
|
|
|
|
|
|
|
} |
|
262
|
|
|
|
|
|
|
} |
|
263
|
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
sub is_recycled { |
|
266
|
4
|
|
|
4
|
1
|
39
|
my $self = shift; |
|
267
|
4
|
50
|
|
|
|
5
|
eval { $self->kdbx } or return FALSE; |
|
|
4
|
|
|
|
|
7
|
|
|
268
|
4
|
|
100
|
5
|
|
9
|
return !!($self->group && any { $_->is_recycle_bin } @{$self->lineage}); |
|
|
5
|
|
|
|
|
12
|
|
|
269
|
|
|
|
|
|
|
} |
|
270
|
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
############################################################################## |
|
272
|
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
|
|
274
|
|
|
|
|
|
|
sub tag_list { |
|
275
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
276
|
0
|
|
0
|
|
|
0
|
return grep { $_ ne '' } split(/[,\.:;]|\s+/, trim($self->tags) // ''); |
|
|
0
|
|
|
|
|
0
|
|
|
277
|
|
|
|
|
|
|
} |
|
278
|
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
sub custom_icon { |
|
281
|
7
|
|
|
7
|
1
|
25
|
my $self = shift; |
|
282
|
7
|
|
|
|
|
14
|
my $kdbx = $self->kdbx; |
|
283
|
7
|
100
|
|
|
|
19
|
if (@_) { |
|
284
|
4
|
|
|
|
|
6
|
my $img = shift; |
|
285
|
4
|
100
|
|
|
|
15
|
my $uuid = defined $img ? $kdbx->add_custom_icon($img, @_) : undef; |
|
286
|
4
|
100
|
|
|
|
24
|
$self->icon_id(0) if $uuid; |
|
287
|
4
|
|
|
|
|
11
|
$self->custom_icon_uuid($uuid); |
|
288
|
4
|
|
|
|
|
14
|
return $img; |
|
289
|
|
|
|
|
|
|
} |
|
290
|
3
|
|
|
|
|
8
|
return $kdbx->custom_icon_data($self->custom_icon_uuid); |
|
291
|
|
|
|
|
|
|
} |
|
292
|
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
|
|
294
|
|
|
|
|
|
|
sub custom_data { |
|
295
|
517
|
|
|
517
|
1
|
713
|
my $self = shift; |
|
296
|
517
|
50
|
66
|
|
|
1151
|
$self->{custom_data} = shift if @_ == 1 && is_plain_hashref($_[0]); |
|
297
|
517
|
100
|
100
|
|
|
2380
|
return $self->{custom_data} //= {} if !@_; |
|
298
|
|
|
|
|
|
|
|
|
299
|
19
|
100
|
|
|
|
70
|
my %args = @_ == 2 ? (key => shift, value => shift) |
|
|
|
100
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
: @_ % 2 == 1 ? (key => shift, @_) : @_; |
|
301
|
|
|
|
|
|
|
|
|
302
|
19
|
50
|
66
|
|
|
40
|
if (!$args{key} && !$args{value}) { |
|
303
|
2
|
|
|
|
|
6
|
my %standard = (key => 1, value => 1, last_modification_time => 1); |
|
304
|
2
|
|
|
|
|
5
|
my @other_keys = grep { !$standard{$_} } keys %args; |
|
|
4
|
|
|
|
|
9
|
|
|
305
|
2
|
50
|
|
|
|
6
|
if (@other_keys == 1) { |
|
306
|
2
|
|
|
|
|
4
|
my $key = $args{key} = $other_keys[0]; |
|
307
|
2
|
|
|
|
|
5
|
$args{value} = delete $args{$key}; |
|
308
|
|
|
|
|
|
|
} |
|
309
|
|
|
|
|
|
|
} |
|
310
|
|
|
|
|
|
|
|
|
311
|
19
|
50
|
|
|
|
36
|
my $key = $args{key} or throw 'Must provide a custom_data key to access'; |
|
312
|
|
|
|
|
|
|
|
|
313
|
19
|
50
|
|
|
|
46
|
return $self->{custom_data}{$key} = $args{value} if is_plain_hashref($args{value}); |
|
314
|
|
|
|
|
|
|
|
|
315
|
19
|
|
|
|
|
53
|
while (my ($field, $value) = each %args) { |
|
316
|
34
|
|
|
|
|
103
|
$self->{custom_data}{$key}{$field} = $value; |
|
317
|
|
|
|
|
|
|
} |
|
318
|
19
|
|
|
|
|
44
|
return $self->{custom_data}{$key}; |
|
319
|
|
|
|
|
|
|
} |
|
320
|
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
sub custom_data_value { |
|
323
|
6
|
|
|
6
|
1
|
51
|
my $self = shift; |
|
324
|
6
|
|
50
|
|
|
13
|
my $data = $self->custom_data(@_) // return undef; |
|
325
|
6
|
|
|
|
|
47
|
return $data->{value}; |
|
326
|
|
|
|
|
|
|
} |
|
327
|
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
############################################################################## |
|
329
|
|
|
|
|
|
|
|
|
330
|
|
|
|
|
|
|
|
|
331
|
|
|
|
|
|
|
sub begin_work { |
|
332
|
35
|
|
|
35
|
1
|
1609
|
my $self = shift; |
|
333
|
|
|
|
|
|
|
|
|
334
|
35
|
100
|
|
|
|
74
|
if (defined wantarray) { |
|
335
|
16
|
|
|
|
|
807
|
require File::KDBX::Transaction; |
|
336
|
16
|
|
|
|
|
56
|
return File::KDBX::Transaction->new($self, @_); |
|
337
|
|
|
|
|
|
|
} |
|
338
|
|
|
|
|
|
|
|
|
339
|
19
|
|
|
|
|
34
|
my %args = @_; |
|
340
|
19
|
|
66
|
|
|
49
|
my $orig = $args{snapshot} // do { |
|
341
|
|
|
|
|
|
|
my $c = $self->clone( |
|
342
|
|
|
|
|
|
|
entries => $args{entries} // 0, |
|
343
|
|
|
|
|
|
|
groups => $args{groups} // 0, |
|
344
|
17
|
|
100
|
|
|
157
|
history => $args{history} // 0, |
|
|
|
|
50
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
345
|
|
|
|
|
|
|
); |
|
346
|
17
|
100
|
|
|
|
51
|
$c->{entries} = $self->{entries} if !$args{entries}; |
|
347
|
17
|
50
|
|
|
|
59
|
$c->{groups} = $self->{groups} if !$args{groups}; |
|
348
|
17
|
50
|
|
|
|
40
|
$c->{history} = $self->{history} if !$args{history}; |
|
349
|
17
|
|
|
|
|
41
|
$c; |
|
350
|
|
|
|
|
|
|
}; |
|
351
|
|
|
|
|
|
|
|
|
352
|
19
|
|
|
|
|
44
|
my $id = Hash::Util::FieldHash::id($orig); |
|
353
|
19
|
|
|
|
|
47
|
_save_references($id, $self, $orig); |
|
354
|
|
|
|
|
|
|
|
|
355
|
19
|
|
|
|
|
64
|
$self->_signal_begin_work; |
|
356
|
|
|
|
|
|
|
|
|
357
|
19
|
|
|
|
|
30
|
push @{$self->_txns}, $orig; |
|
|
19
|
|
|
|
|
30
|
|
|
358
|
|
|
|
|
|
|
} |
|
359
|
|
|
|
|
|
|
|
|
360
|
|
|
|
|
|
|
|
|
361
|
|
|
|
|
|
|
sub commit { |
|
362
|
14
|
|
|
14
|
1
|
24
|
my $self = shift; |
|
363
|
14
|
50
|
|
|
|
19
|
my $orig = pop @{$self->_txns} or return $self; |
|
|
14
|
|
|
|
|
24
|
|
|
364
|
14
|
|
|
|
|
50
|
$self->_commit($orig); |
|
365
|
14
|
|
|
|
|
191
|
my $signals = $self->_signal_commit; |
|
366
|
14
|
100
|
|
|
|
32
|
$self->_signal_send($signals) if !$self->_in_txn; |
|
367
|
14
|
|
|
|
|
83
|
return $self; |
|
368
|
|
|
|
|
|
|
} |
|
369
|
|
|
|
|
|
|
|
|
370
|
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
sub rollback { |
|
372
|
5
|
|
|
5
|
1
|
7
|
my $self = shift; |
|
373
|
|
|
|
|
|
|
|
|
374
|
5
|
50
|
|
|
|
8
|
my $orig = pop @{$self->_txns} or return $self; |
|
|
5
|
|
|
|
|
11
|
|
|
375
|
|
|
|
|
|
|
|
|
376
|
5
|
|
|
|
|
12
|
my $id = Hash::Util::FieldHash::id($orig); |
|
377
|
5
|
|
|
|
|
12
|
_restore_references($id, $orig); |
|
378
|
|
|
|
|
|
|
|
|
379
|
5
|
|
|
|
|
17
|
$self->_signal_rollback; |
|
380
|
|
|
|
|
|
|
|
|
381
|
5
|
|
|
|
|
45
|
return $self; |
|
382
|
|
|
|
|
|
|
} |
|
383
|
|
|
|
|
|
|
|
|
384
|
|
|
|
|
|
|
# Get whether or not there is at least one pending transaction. |
|
385
|
82
|
|
|
82
|
|
96
|
sub _in_txn { scalar @{$_[0]->_txns} } |
|
|
82
|
|
|
|
|
163
|
|
|
386
|
|
|
|
|
|
|
|
|
387
|
|
|
|
|
|
|
# Get an array ref of pending transactions. |
|
388
|
152
|
|
100
|
152
|
|
831
|
sub _txns { $TXNS{$_[0]} //= [] } |
|
389
|
|
|
|
|
|
|
|
|
390
|
|
|
|
|
|
|
# The _commit hook notifies subclasses that a commit has occurred. |
|
391
|
0
|
|
|
0
|
|
0
|
sub _commit { die 'Not implemented' } |
|
392
|
|
|
|
|
|
|
|
|
393
|
|
|
|
|
|
|
# Get a reference to an object that represents an object's committed state. If there is no pending |
|
394
|
|
|
|
|
|
|
# transaction, this is just $self. If there is a transaction, this is the snapshot take before the transaction |
|
395
|
|
|
|
|
|
|
# began. This method is private because it provides direct access to the actual snapshot. It is important that |
|
396
|
|
|
|
|
|
|
# the snapshot not be changed or a rollback would roll back to an altered state. |
|
397
|
|
|
|
|
|
|
# This is used by File::KDBX::Dumper::XML so as to not dump uncommitted changes. |
|
398
|
|
|
|
|
|
|
sub _committed { |
|
399
|
32
|
|
|
32
|
|
57
|
my $self = shift; |
|
400
|
32
|
|
|
|
|
35
|
my ($orig) = @{$self->_txns}; |
|
|
32
|
|
|
|
|
76
|
|
|
401
|
32
|
|
66
|
|
|
195
|
return $orig // $self; |
|
402
|
|
|
|
|
|
|
} |
|
403
|
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
# In addition to cloning an object when beginning work, we also keep track its hashrefs and arrayrefs |
|
405
|
|
|
|
|
|
|
# internally so that we can restore to the very same structures in the case of a rollback. |
|
406
|
|
|
|
|
|
|
sub _save_references { |
|
407
|
1406
|
|
|
1406
|
|
1542
|
my $id = shift; |
|
408
|
1406
|
|
|
|
|
1448
|
my $self = shift; |
|
409
|
1406
|
|
|
|
|
1406
|
my $orig = shift; |
|
410
|
|
|
|
|
|
|
|
|
411
|
1406
|
100
|
100
|
|
|
4118
|
if (is_plain_arrayref($orig)) { |
|
|
|
100
|
66
|
|
|
|
|
|
412
|
67
|
|
|
|
|
132
|
for (my $i = 0; $i < @$orig; ++$i) { |
|
413
|
20
|
|
|
|
|
38
|
_save_references($id, $self->[$i], $orig->[$i]); |
|
414
|
|
|
|
|
|
|
} |
|
415
|
67
|
|
|
|
|
229
|
$REFS{$id}{Hash::Util::FieldHash::id($orig)} = $self; |
|
416
|
|
|
|
|
|
|
} |
|
417
|
|
|
|
|
|
|
elsif (is_plain_hashref($orig) || (blessed $orig && $orig->isa(__PACKAGE__))) { |
|
418
|
349
|
|
|
|
|
640
|
for my $key (keys %$orig) { |
|
419
|
1367
|
|
|
|
|
1928
|
_save_references($id, $self->{$key}, $orig->{$key}); |
|
420
|
|
|
|
|
|
|
} |
|
421
|
349
|
|
|
|
|
1039
|
$REFS{$id}{Hash::Util::FieldHash::id($orig)} = $self; |
|
422
|
|
|
|
|
|
|
} |
|
423
|
|
|
|
|
|
|
} |
|
424
|
|
|
|
|
|
|
|
|
425
|
|
|
|
|
|
|
# During a rollback, copy data from the snapshot back into the original internal structures. |
|
426
|
|
|
|
|
|
|
sub _restore_references { |
|
427
|
377
|
|
|
377
|
|
388
|
my $id = shift; |
|
428
|
377
|
|
100
|
|
|
590
|
my $orig = shift // return; |
|
429
|
329
|
|
50
|
|
|
1643
|
my $self = delete $REFS{$id}{Hash::Util::FieldHash::id($orig) // ''} // return $orig; |
|
|
|
|
100
|
|
|
|
|
|
430
|
|
|
|
|
|
|
|
|
431
|
108
|
100
|
33
|
|
|
262
|
if (is_plain_arrayref($orig)) { |
|
|
|
50
|
66
|
|
|
|
|
|
432
|
19
|
|
|
|
|
36
|
@$self = map { _restore_references($id, $_) } @$orig; |
|
|
6
|
|
|
|
|
10
|
|
|
433
|
|
|
|
|
|
|
} |
|
434
|
|
|
|
|
|
|
elsif (is_plain_hashref($orig) || (blessed $orig && $orig->isa(__PACKAGE__))) { |
|
435
|
89
|
|
|
|
|
167
|
for my $key (keys %$orig) { |
|
436
|
|
|
|
|
|
|
# next if is_ref($orig->{$key}) && |
|
437
|
|
|
|
|
|
|
# (Hash::Util::FieldHash::id($self->{$key}) // 0) == Hash::Util::FieldHash::id($orig->{$key}); |
|
438
|
366
|
|
|
|
|
495
|
$self->{$key} = _restore_references($id, $orig->{$key}); |
|
439
|
|
|
|
|
|
|
} |
|
440
|
|
|
|
|
|
|
} |
|
441
|
|
|
|
|
|
|
|
|
442
|
108
|
|
|
|
|
189
|
return $self; |
|
443
|
|
|
|
|
|
|
} |
|
444
|
|
|
|
|
|
|
|
|
445
|
|
|
|
|
|
|
############################################################################## |
|
446
|
|
|
|
|
|
|
|
|
447
|
|
|
|
|
|
|
sub _signal { |
|
448
|
68
|
|
|
68
|
|
91
|
my $self = shift; |
|
449
|
68
|
|
|
|
|
90
|
my $type = shift; |
|
450
|
|
|
|
|
|
|
|
|
451
|
68
|
50
|
|
|
|
151
|
if ($self->_in_txn) { |
|
452
|
0
|
|
|
|
|
0
|
my $stack = $self->_signal_stack; |
|
453
|
0
|
|
|
|
|
0
|
my $queue = $stack->[-1]; |
|
454
|
0
|
|
|
|
|
0
|
push @$queue, [$type, @_]; |
|
455
|
|
|
|
|
|
|
} |
|
456
|
|
|
|
|
|
|
|
|
457
|
68
|
|
|
|
|
265
|
$self->_signal_send([[$type, @_]]); |
|
458
|
|
|
|
|
|
|
|
|
459
|
68
|
|
|
|
|
278
|
return $self; |
|
460
|
|
|
|
|
|
|
} |
|
461
|
|
|
|
|
|
|
|
|
462
|
52
|
|
100
|
52
|
|
226
|
sub _signal_stack { $SIGNALS{$_[0]} //= [] } |
|
463
|
|
|
|
|
|
|
|
|
464
|
|
|
|
|
|
|
sub _signal_begin_work { |
|
465
|
19
|
|
|
19
|
|
25
|
my $self = shift; |
|
466
|
19
|
|
|
|
|
25
|
push @{$self->_signal_stack}, []; |
|
|
19
|
|
|
|
|
43
|
|
|
467
|
|
|
|
|
|
|
} |
|
468
|
|
|
|
|
|
|
|
|
469
|
|
|
|
|
|
|
sub _signal_commit { |
|
470
|
14
|
|
|
14
|
|
23
|
my $self = shift; |
|
471
|
14
|
|
|
|
|
25
|
my $signals = pop @{$self->_signal_stack}; |
|
|
14
|
|
|
|
|
31
|
|
|
472
|
14
|
|
100
|
|
|
28
|
my $previous = $self->_signal_stack->[-1] // []; |
|
473
|
14
|
|
|
|
|
30
|
push @$previous, @$signals; |
|
474
|
14
|
|
|
|
|
22
|
return $previous; |
|
475
|
|
|
|
|
|
|
} |
|
476
|
|
|
|
|
|
|
|
|
477
|
|
|
|
|
|
|
sub _signal_rollback { |
|
478
|
5
|
|
|
5
|
|
8
|
my $self = shift; |
|
479
|
5
|
|
|
|
|
5
|
pop @{$self->_signal_stack}; |
|
|
5
|
|
|
|
|
11
|
|
|
480
|
|
|
|
|
|
|
} |
|
481
|
|
|
|
|
|
|
|
|
482
|
|
|
|
|
|
|
sub _signal_send { |
|
483
|
80
|
|
|
80
|
|
128
|
my $self = shift; |
|
484
|
80
|
|
50
|
|
|
156
|
my $signals = shift // []; |
|
485
|
|
|
|
|
|
|
|
|
486
|
80
|
50
|
|
|
|
199
|
my $kdbx = $KDBX{$self} or return; |
|
487
|
|
|
|
|
|
|
|
|
488
|
|
|
|
|
|
|
# de-duplicate, keeping the most recent signal for each type |
|
489
|
80
|
|
|
|
|
103
|
my %seen; |
|
490
|
80
|
|
|
|
|
149
|
my @signals = grep { !$seen{$_->[0]}++ } reverse @$signals; |
|
|
68
|
|
|
|
|
263
|
|
|
491
|
|
|
|
|
|
|
|
|
492
|
80
|
|
|
|
|
157
|
for my $sig (reverse @signals) { |
|
493
|
68
|
|
|
|
|
185
|
$kdbx->_handle_signal($self, @$sig); |
|
494
|
|
|
|
|
|
|
} |
|
495
|
|
|
|
|
|
|
} |
|
496
|
|
|
|
|
|
|
|
|
497
|
|
|
|
|
|
|
############################################################################## |
|
498
|
|
|
|
|
|
|
|
|
499
|
|
|
|
|
|
|
sub _wrap_group { |
|
500
|
54
|
|
|
54
|
|
71
|
my $self = shift; |
|
501
|
54
|
|
|
|
|
69
|
my $group = shift; |
|
502
|
54
|
|
|
|
|
166
|
require File::KDBX::Group; |
|
503
|
54
|
|
|
|
|
135
|
return File::KDBX::Group->wrap($group, $KDBX{$self}); |
|
504
|
|
|
|
|
|
|
} |
|
505
|
|
|
|
|
|
|
|
|
506
|
|
|
|
|
|
|
sub _wrap_entry { |
|
507
|
59
|
|
|
59
|
|
88
|
my $self = shift; |
|
508
|
59
|
|
|
|
|
117
|
my $entry = shift; |
|
509
|
59
|
|
|
|
|
3299
|
require File::KDBX::Entry; |
|
510
|
59
|
|
|
|
|
264
|
return File::KDBX::Entry->wrap($entry, $KDBX{$self}); |
|
511
|
|
|
|
|
|
|
} |
|
512
|
|
|
|
|
|
|
|
|
513
|
0
|
|
|
0
|
0
|
|
sub TO_JSON { +{%{$_[0]}} } |
|
|
0
|
|
|
|
|
|
|
|
514
|
|
|
|
|
|
|
|
|
515
|
|
|
|
|
|
|
1; |
|
516
|
|
|
|
|
|
|
|
|
517
|
|
|
|
|
|
|
__END__ |