]> Dogcows Code - chaz/p5-File-KDBX/blobdiff - lib/File/KDBX/Object.pm
Remove min_version and clean up a lot of pod
[chaz/p5-File-KDBX] / lib / File / KDBX / Object.pm
index e1c8c8e1b2e9f8d841f8ab41b66f77254d664632..63eadcca0dbd03da9881221e2ef2b54c35c59ede 100644 (file)
@@ -5,16 +5,18 @@ use warnings;
 use strict;
 
 use Devel::GlobalDestruction;
 use strict;
 
 use Devel::GlobalDestruction;
+use File::KDBX::Constants qw(:bool);
 use File::KDBX::Error;
 use File::KDBX::Util qw(:uuid);
 use File::KDBX::Error;
 use File::KDBX::Util qw(:uuid);
-use Ref::Util qw(is_arrayref is_plain_hashref is_ref);
-use Scalar::Util qw(blessed refaddr weaken);
+use Hash::Util::FieldHash qw(fieldhashes);
+use List::Util qw(any first);
+use Ref::Util qw(is_arrayref is_plain_arrayref is_plain_hashref is_ref);
+use Scalar::Util qw(blessed weaken);
 use namespace::clean;
 
 our $VERSION = '999.999'; # VERSION
 
 use namespace::clean;
 
 our $VERSION = '999.999'; # VERSION
 
-my %KDBX;
-my %PARENT;
+fieldhashes \my (%KDBX, %PARENT, %TXNS, %REFS, %SIGNALS);
 
 =method new
 
 
 =method new
 
@@ -72,11 +74,11 @@ sub new {
 
     my $self = bless $data // {}, $class;
     $self->init(%args);
 
     my $self = bless $data // {}, $class;
     $self->init(%args);
-    $self->_set_default_attributes if !$data;
+    $self->_set_nonlazy_attributes if !$data;
     return $self;
 }
 
     return $self;
 }
 
-sub _set_default_attributes { die 'Not implemented' }
+sub _set_nonlazy_attributes { die 'Not implemented' }
 
 =method init
 
 
 =method init
 
@@ -99,13 +101,6 @@ sub init {
     return $self;
 }
 
     return $self;
 }
 
-sub DESTROY {
-    return if in_global_destruction;
-    my $self = shift;
-    delete $KDBX{refaddr($self)};
-    delete $PARENT{refaddr($self)};
-}
-
 =method wrap
 
     $object = File::KDBX::Object->wrap($object);
 =method wrap
 
     $object = File::KDBX::Object->wrap($object);
@@ -136,12 +131,12 @@ sub label { die 'Not implemented' }
 
 =method clone
 
 
 =method clone
 
-    $object_copy = $object->clone;
+    $object_copy = $object->clone(%options);
     $object_copy = File::KDBX::Object->new($object);
 
     $object_copy = File::KDBX::Object->new($object);
 
-Make a clone of an object. By default the clone is indeed an exact copy that is associated with the same
-database but not actually included in the object tree (i.e. it has no parent). Some options are allowed to
-get different effects:
+Make a clone of an object. By default the clone is indeed an exact copy that is connected to the same database
+but not actually included in the object tree (i.e. it has no parent group). Some options are allowed to get
+different effects:
 
 =for :list
 * C<new_uuid> - If set, generate a new UUID for the copy (default: false)
 
 =for :list
 * C<new_uuid> - If set, generate a new UUID for the copy (default: false)
@@ -175,7 +170,7 @@ sub clone {
     if ($args{relabel} and my $label = $self->label) {
         $copy->label("$label - Copy");
     }
     if ($args{relabel} and my $label = $self->label) {
         $copy->label("$label - Copy");
     }
-    if ($args{parent} and my $parent = $self->parent) {
+    if ($args{parent} and my $parent = $self->group) {
         $parent->add_object($copy);
     }
 
         $parent->add_object($copy);
     }
 
@@ -191,19 +186,21 @@ sub STORABLE_freeze {
     delete $copy->{groups}  if !$CLONE{groups};
     delete $copy->{history} if !$CLONE{history};
 
     delete $copy->{groups}  if !$CLONE{groups};
     delete $copy->{history} if !$CLONE{history};
 
-    return refaddr($self) || '', $copy;
+    return ($cloning ? Hash::Util::FieldHash::id($self) : ''), $copy;
 }
 
 sub STORABLE_thaw {
     my $self    = shift;
     my $cloning = shift;
     my $addr    = shift;
 }
 
 sub STORABLE_thaw {
     my $self    = shift;
     my $cloning = shift;
     my $addr    = shift;
-    my $clone   = shift;
+    my $copy    = shift;
 
 
-    @$self{keys %$clone} = values %$clone;
+    @$self{keys %$copy} = values %$copy;
 
 
-    my $kdbx = $KDBX{$addr};
-    $self->kdbx($kdbx) if $kdbx;
+    if ($cloning) {
+        my $kdbx = $KDBX{$addr};
+        $self->kdbx($kdbx) if $kdbx;
+    }
 
     if (defined $self->{uuid}) {
         if (($CLONE{reference_password} || $CLONE{reference_username}) && $self->can('strings')) {
 
     if (defined $self->{uuid}) {
         if (($CLONE{reference_password} || $CLONE{reference_username}) && $self->can('strings')) {
@@ -215,9 +212,10 @@ sub STORABLE_thaw {
                 local $CLONE{history}               = 1;
                 local $CLONE{reference_password}    = 0;
                 local $CLONE{reference_username}    = 0;
                 local $CLONE{history}               = 1;
                 local $CLONE{reference_password}    = 0;
                 local $CLONE{reference_username}    = 0;
-                bless Storable::dclone({%$clone}), 'File::KDBX::Entry';
+                # Clone only the entry's data and manually bless to avoid infinite recursion.
+                bless Storable::dclone({%$copy}), 'File::KDBX::Entry';
             };
             };
-            my $txn = $self->begin_work($clone_obj);
+            my $txn = $self->begin_work(snapshot => $clone_obj);
             if ($CLONE{reference_password}) {
                 $self->password("{REF:P\@I:$uuid}");
             }
             if ($CLONE{reference_password}) {
                 $self->password("{REF:P\@I:$uuid}");
             }
@@ -228,6 +226,9 @@ sub STORABLE_thaw {
         }
         $self->uuid(generate_uuid) if $CLONE{new_uuid};
     }
         }
         $self->uuid(generate_uuid) if $CLONE{new_uuid};
     }
+
+    # Dualvars aren't cloned as dualvars, so dualify the icon.
+    $self->icon_id($self->{icon_id}) if defined $self->{icon_id};
 }
 
 =attr kdbx
 }
 
 =attr kdbx
@@ -235,24 +236,38 @@ sub STORABLE_thaw {
     $kdbx = $object->kdbx;
     $object->kdbx($kdbx);
 
     $kdbx = $object->kdbx;
     $object->kdbx($kdbx);
 
-Get or set the L<File::KDBX> instance associated with this object.
+Get or set the L<File::KDBX> instance connected with this object. Throws if the object is disconnected. Other
+object methods might only work if the object is connected to a database and so they might also throw if the
+object is disconnected. If you're not sure if an object is connected, try L</is_connected>.
 
 =cut
 
 sub kdbx {
     my $self = shift;
     $self = $self->new if !ref $self;
 
 =cut
 
 sub kdbx {
     my $self = shift;
     $self = $self->new if !ref $self;
-    my $addr = refaddr($self);
     if (@_) {
     if (@_) {
-        $KDBX{$addr} = shift;
-        if (defined $KDBX{$addr}) {
-            weaken $KDBX{$addr};
+        if (my $kdbx = shift) {
+            $KDBX{$self} = $kdbx;
+            weaken $KDBX{$self};
         }
         else {
         }
         else {
-            delete $KDBX{$addr};
+            delete $KDBX{$self};
         }
     }
         }
     }
-    $KDBX{$addr} or throw 'Object is disassociated from a KDBX database', object => $self;
+    $KDBX{$self} or throw 'Object is disconnected', object => $self;
+}
+
+=method is_connected
+
+    $bool = $object->is_connected;
+
+Determine whether or not an object is connected to a database.
+
+=cut
+
+sub is_connected {
+    my $self = shift;
+    return !!eval { $self->kdbx };
 }
 
 =method id
 }
 
 =method id
@@ -273,42 +288,46 @@ sub id { format_uuid(shift->uuid, @_) }
 
 =method group
 
 
 =method group
 
-=method parent
-
-    $group = $object->group;
-    # OR equivalently
-    $group = $object->parent;
+    $parent_group = $object->group;
+    $object->group($parent_group);
 
 
-Get the parent group to which an object belongs or C<undef> if it belongs to no group.
+Get or set the parent group to which an object belongs or C<undef> if it belongs to no group.
 
 =cut
 
 sub group {
     my $self = shift;
 
 =cut
 
 sub group {
     my $self = shift;
-    my $addr = refaddr($self);
-    if (my $group = $PARENT{$addr}) {
+
+    if (my $new_group = shift) {
+        my $old_group = $self->group;
+        return $new_group if Hash::Util::FieldHash::id($old_group) == Hash::Util::FieldHash::id($new_group);
+        # move to a new parent
+        $self->remove(signal => 0) if $old_group;
+        $self->location_changed('now');
+        $new_group->add_object($self);
+    }
+
+    my $id   = Hash::Util::FieldHash::id($self);
+    if (my $group = $PARENT{$self}) {
         my $method = $self->_parent_container;
         my $method = $self->_parent_container;
-        for my $object (@{$group->$method}) {
-            return $group if $addr == refaddr($object);
-        }
-        delete $PARENT{$addr};
+        return $group if first { $id == Hash::Util::FieldHash::id($_) } @{$group->$method};
+        delete $PARENT{$self};
     }
     # always get lineage from root to leaf because the other way requires parent, so it would be recursive
     my $lineage = $self->kdbx->_trace_lineage($self) or return;
     my $group = pop @$lineage or return;
     }
     # always get lineage from root to leaf because the other way requires parent, so it would be recursive
     my $lineage = $self->kdbx->_trace_lineage($self) or return;
     my $group = pop @$lineage or return;
-    $PARENT{$addr} = $group; weaken $PARENT{$addr};
+    $PARENT{$self} = $group; weaken $PARENT{$self};
     return $group;
 }
 
     return $group;
 }
 
-sub parent { shift->group(@_) }
-
 sub _set_group {
     my $self = shift;
     if (my $parent = shift) {
 sub _set_group {
     my $self = shift;
     if (my $parent = shift) {
-        $PARENT{refaddr($self)} = $parent;
+        $PARENT{$self} = $parent;
+        weaken $PARENT{$self};
     }
     else {
     }
     else {
-        delete $PARENT{refaddr($self)};
+        delete $PARENT{$self};
     }
     return $self;
 }
     }
     return $self;
 }
@@ -331,16 +350,16 @@ sub lineage {
     my $self = shift;
     my $base = shift;
 
     my $self = shift;
     my $base = shift;
 
-    my $base_addr = $base ? refaddr($base) : 0;
+    my $base_addr = $base ? Hash::Util::FieldHash::id($base) : 0;
 
     # try leaf to root
     my @path;
 
     # try leaf to root
     my @path;
-    my $o = $self;
-    while ($o = $o->parent) {
-        unshift @path, $o;
-        last if $base_addr == refaddr($o);
+    my $object = $self;
+    while ($object = $object->group) {
+        unshift @path, $object;
+        last if $base_addr == Hash::Util::FieldHash::id($object);
     }
     }
-    return \@path if @path && ($base_addr == refaddr($path[0]) || $path[0]->is_root);
+    return \@path if @path && ($base_addr == Hash::Util::FieldHash::id($path[0]) || $path[0]->is_root);
 
     # try root to leaf
     return $self->kdbx->_trace_lineage($self, $base);
 
     # try root to leaf
     return $self->kdbx->_trace_lineage($self, $base);
@@ -348,19 +367,73 @@ sub lineage {
 
 =method remove
 
 
 =method remove
 
-    $object = $object->remove;
+    $object = $object->remove(%options);
 
 
-Remove the object from the database. If the object is a group, all contained objects are removed as well.
+Remove an object from its parent. If the object is a group, all contained objects stay with the object and so
+are removed as well, just like cutting off a branch takes the leafs as well. Options:
+
+=for :list
+* C<signal> Whether or not to signal the removal to the connected database (default: true)
 
 =cut
 
 sub remove {
     my $self = shift;
 
 =cut
 
 sub remove {
     my $self = shift;
-    my $parent = $self->parent;
-    $parent->remove_object($self) if $parent;
+    my $parent = $self->group;
+    $parent->remove_object($self, @_) if $parent;
+    $self->_set_group(undef);
     return $self;
 }
 
     return $self;
 }
 
+=method recycle
+
+    $object = $object->recycle;
+
+Remove an object from its parent and add it to the connected database's recycle bin group.
+
+=cut
+
+sub recycle {
+    my $self = shift;
+    return $self->group($self->kdbx->recycle_bin);
+}
+
+=method recycle_or_remove
+
+    $object = $object->recycle_or_remove;
+
+Recycle or remove an object, depending on the connected database's L<File::KDBX/recycle_bin_enabled>. If the
+object is not connected to a database or is already in the recycle bin, remove it.
+
+=cut
+
+sub recycle_or_remove {
+    my $self = shift;
+    my $kdbx = eval { $self->kdbx };
+    if ($kdbx && $kdbx->recycle_bin_enabled && !$self->is_recycled) {
+        $self->recycle;
+    }
+    else {
+        $self->remove;
+    }
+}
+
+=method is_recycled
+
+    $bool = $object->is_recycled;
+
+Get whether or not an object is in a recycle bin.
+
+=cut
+
+sub is_recycled {
+    my $self = shift;
+    eval { $self->kdbx } or return FALSE;
+    return !!($self->group && any { $_->is_recycle_bin } @{$self->lineage});
+}
+
+##############################################################################
+
 =method tag_list
 
     @tags = $entry->tag_list;
 =method tag_list
 
     @tags = $entry->tag_list;
@@ -413,7 +486,9 @@ sub custom_icon {
     $object->custom_data(%data);
     $object->custom_data(key => $value, %data);
 
     $object->custom_data(%data);
     $object->custom_data(key => $value, %data);
 
-Get and set custom data. Custom data is metadata associated with an object.
+Get and set custom data. Custom data is metadata associated with an object. It is a set of key-value pairs
+used to store arbitrary data, usually used by software like plug-ins to keep track of state rather than by end
+users.
 
 Each data item can have a few attributes associated with it.
 
 
 Each data item can have a few attributes associated with it.
 
@@ -469,18 +544,243 @@ sub custom_data_value {
     return $data->{value};
 }
 
     return $data->{value};
 }
 
+##############################################################################
+
+=method begin_work
+
+    $txn = $object->begin_work(%options);
+    $object->begin_work(%options);
+
+Begin a new transaction. Returns a L<File::KDBX::Transaction> object that can be scoped to ensure a rollback
+occurs if exceptions are thrown. Alternatively, if called in void context, there will be no
+B<File::KDBX::Transaction> and it is instead your responsibility to call L</commit> or L</rollback> as
+appropriate. It is undefined behavior to call these if a B<File::KDBX::Transaction> exists. Recursive
+transactions are allowed.
+
+Signals created during a transaction are delayed until all transactions are resolved. If the outermost
+transaction is committed, then the signals are de-duplicated and delivered. Otherwise the signals are dropped.
+This means that the KDBX database will not fix broken references or mark itself dirty until after the
+transaction is committed.
+
+How it works: With the beginning of a transaction, a snapshot of the object is created. In the event of
+a rollback, the object's data is replaced with data from the snapshot.
+
+By default, the snapshot is shallow (i.e. does not include subroups, entries or historical entries). This
+means that only modifications to the object itself (its data, fields, strings, etc.) are atomic; modifications
+to subroups etc., including adding or removing items, are auto-committed instantly and will persist regardless
+of the result of the pending transaction. You can override this for groups, entries and history independently
+using options:
+
+=for :list
+* C<entries> - If set, snapshot entries within a group, deeply (default: false)
+* C<groups> - If set, snapshot subroups within a group, deeply (default: false)
+* C<history> - If set, snapshot historical entries within an entry (default: false)
+
+For example, if you begin a transaction on a group object using the C<entries> option, like this:
+
+    $group->begin_work(entries => 1);
+
+Then if you modify any of the group's entries OR add new entries OR delete entries, all of that will be undone
+if the transaction is rolled back. With a default-configured transaction, however, changes to entries are kept
+even if the transaction is rolled back.
+
+=cut
+
+sub begin_work {
+    my $self = shift;
+
+    if (defined wantarray) {
+        require File::KDBX::Transaction;
+        return File::KDBX::Transaction->new($self, @_);
+    }
+
+    my %args = @_;
+    my $orig = $args{snapshot} // do {
+        my $c = $self->clone(
+            entries => $args{entries} // 0,
+            groups  => $args{groups}  // 0,
+            history => $args{history} // 0,
+        );
+        $c->{entries} = $self->{entries} if !$args{entries};
+        $c->{groups}  = $self->{groups}  if !$args{groups};
+        $c->{history} = $self->{history} if !$args{history};
+        $c;
+    };
+
+    my $id = Hash::Util::FieldHash::id($orig);
+    _save_references($id, $self, $orig);
+
+    $self->_signal_begin_work;
+
+    push @{$self->_txns}, $orig;
+}
+
+=method commit
+
+    $object->commit;
+
+Commit a transaction, making updates to C<$object> permanent. Returns itself to allow method chaining.
+
+=cut
+
+sub commit {
+    my $self = shift;
+    my $orig = pop @{$self->_txns} or return $self;
+    $self->_commit($orig);
+    my $signals = $self->_signal_commit;
+    $self->_signal_send($signals) if !$self->_in_txn;
+    return $self;
+}
+
+=method rollback
+
+    $object->rollback;
+
+Roll back the most recent transaction, throwing away any updates to the L</object> made since the transaction
+began. Returns itself to allow method chaining.
+
+=cut
+
+sub rollback {
+    my $self = shift;
+
+    my $orig = pop @{$self->_txns} or return $self;
+
+    my $id = Hash::Util::FieldHash::id($orig);
+    _restore_references($id, $orig);
+
+    $self->_signal_rollback;
+
+    return $self;
+}
+
+# Get whether or not there is at least one pending transaction.
+sub _in_txn { scalar @{$_[0]->_txns} }
+
+# Get an array ref of pending transactions.
+sub _txns   { $TXNS{$_[0]} //= [] }
+
+# The _commit hook notifies subclasses that a commit has occurred.
+sub _commit { die 'Not implemented' }
+
+# Get a reference to an object that represents an object's committed state. If there is no pending
+# transaction, this is just $self. If there is a transaction, this is the snapshot take before the transaction
+# began. This method is private because it provides direct access to the actual snapshot. It is important that
+# the snapshot not be changed or a rollback would roll back to an altered state.
+# This is used by File::KDBX::Dumper::XML so as to not dump uncommitted changes.
+sub _committed {
+    my $self = shift;
+    my ($orig) = @{$self->_txns};
+    return $orig // $self;
+}
+
+# In addition to cloning an object when beginning work, we also keep track its hashrefs and arrayrefs
+# internally so that we can restore to the very same structures in the case of a rollback.
+sub _save_references {
+    my $id   = shift;
+    my $self = shift;
+    my $orig = shift;
+
+    if (is_plain_arrayref($orig)) {
+        for (my $i = 0; $i < @$orig; ++$i) {
+            _save_references($id, $self->[$i], $orig->[$i]);
+        }
+        $REFS{$id}{Hash::Util::FieldHash::id($orig)} = $self;
+    }
+    elsif (is_plain_hashref($orig) || (blessed $orig && $orig->isa(__PACKAGE__))) {
+        for my $key (keys %$orig) {
+            _save_references($id, $self->{$key}, $orig->{$key});
+        }
+        $REFS{$id}{Hash::Util::FieldHash::id($orig)} = $self;
+    }
+}
+
+# During a rollback, copy data from the snapshot back into the original internal structures.
+sub _restore_references {
+    my $id   = shift;
+    my $orig = shift // return;
+    my $self = delete $REFS{$id}{Hash::Util::FieldHash::id($orig) // ''} // return $orig;
+
+    if (is_plain_arrayref($orig)) {
+        @$self = map { _restore_references($id, $_) } @$orig;
+    }
+    elsif (is_plain_hashref($orig) || (blessed $orig && $orig->isa(__PACKAGE__))) {
+        for my $key (keys %$orig) {
+            # next if is_ref($orig->{$key}) &&
+            #     (Hash::Util::FieldHash::id($self->{$key}) // 0) == Hash::Util::FieldHash::id($orig->{$key});
+            $self->{$key} = _restore_references($id, $orig->{$key});
+        }
+    }
+
+    return $self;
+}
+
+##############################################################################
+
+sub _signal {
+    my $self = shift;
+    my $type = shift;
+
+    if ($self->_in_txn) {
+        my $stack = $self->_signal_stack;
+        my $queue = $stack->[-1];
+        push @$queue, [$type, @_];
+    }
+
+    $self->_signal_send([[$type, @_]]);
+
+    return $self;
+}
+
+sub _signal_stack { $SIGNALS{$_[0]} //= [] }
+
+sub _signal_begin_work {
+    my $self = shift;
+    push @{$self->_signal_stack}, [];
+}
+
+sub _signal_commit {
+    my $self = shift;
+    my $signals = pop @{$self->_signal_stack};
+    my $previous = $self->_signal_stack->[-1] // [];
+    push @$previous, @$signals;
+    return $previous;
+}
+
+sub _signal_rollback {
+    my $self = shift;
+    pop @{$self->_signal_stack};
+}
+
+sub _signal_send {
+    my $self    = shift;
+    my $signals = shift // [];
+
+    my $kdbx = $KDBX{$self} or return;
+
+    # de-duplicate, keeping the most recent signal for each type
+    my %seen;
+    my @signals = grep { !$seen{$_->[0]}++ } reverse @$signals;
+
+    for my $sig (reverse @signals) {
+        $kdbx->_handle_signal($self, @$sig);
+    }
+}
+
+##############################################################################
+
 sub _wrap_group {
     my $self  = shift;
     my $group = shift;
     require File::KDBX::Group;
 sub _wrap_group {
     my $self  = shift;
     my $group = shift;
     require File::KDBX::Group;
-    return File::KDBX::Group->wrap($group, $KDBX{refaddr($self)});
+    return File::KDBX::Group->wrap($group, $KDBX{$self});
 }
 
 sub _wrap_entry {
     my $self  = shift;
     my $entry = shift;
     require File::KDBX::Entry;
 }
 
 sub _wrap_entry {
     my $self  = shift;
     my $entry = shift;
     require File::KDBX::Entry;
-    return File::KDBX::Entry->wrap($entry, $KDBX{refaddr($self)});
+    return File::KDBX::Entry->wrap($entry, $KDBX{$self});
 }
 
 sub TO_JSON { +{%{$_[0]}} }
 }
 
 sub TO_JSON { +{%{$_[0]}} }
@@ -501,4 +801,90 @@ but instead use its subclasses:
 
 There is some functionality shared by both types of objects, and that's what this class provides.
 
 
 There is some functionality shared by both types of objects, and that's what this class provides.
 
+Each object can be connected with a L<File::KDBX> database or be disconnected. A disconnected object exists in
+memory but will not be persisted when dumping a database. It is also possible for an object to be connected
+with a database but not be part of the object tree (i.e. is not the root group or any subroup or entry).
+A disconnected object or an object not part of the object tree of a database can be added to a database using
+one of:
+
+=for :list
+* L<File::KDBX/add_entry>
+* L<File::KDBX/add_group>
+* L<File::KDBX::Group/add_entry>
+* L<File::KDBX::Group/add_group>
+* L<File::KDBX::Entry/add_historical_entry>
+
+It is possible to copy or move objects between databases, but B<DO NOT> include the same object in more
+than one database at once or there could be some strange aliasing effects (i.e. changes in one database might
+effect another in unexpected ways). This could lead to difficult-to-debug problems. It is similarly not safe
+or valid to add the same object multiple times to the same database. For example:
+
+    my $entry = File::KDBX::Entry->(title => 'Whatever');
+
+    # DO NOT DO THIS:
+    $kdbx->add_entry($entry);
+    $another_kdbx->add_entry($entry);
+
+    # DO NOT DO THIS:
+    $kdbx->add_entry($entry);
+    $kdbx->add_entry($entry); # again
+
+Instead, do this:
+
+    # Copy an entry to multiple databases:
+    $kdbx->add_entry($entry);
+    $another_kdbx->add_entry($entry->clone);
+
+    # OR move an existing entry from one database to another:
+    $another_kdbx->add_entry($entry->remove);
+
+=attr uuid
+
+128-bit UUID identifying the object within the connected database.
+
+=attr icon_id
+
+Integer representing a default icon. See L<File::KDBX::Constants/":icon"> for valid values.
+
+=attr custom_icon_uuid
+
+128-bit UUID identifying a custom icon within the connected database.
+
+=attr tags
+
+Text string with arbitrary tags which can be used to build a taxonomy.
+
+=attr previous_parent_group
+
+128-bit UUID identifying a group within the connected database the previously contained the object.
+
+=attr last_modification_time
+
+Date and time when the entry was last modified.
+
+=attr creation_time
+
+Date and time when the entry was created.
+
+=attr last_access_time
+
+Date and time when the entry was last accessed.
+
+=attr expiry_time
+
+Date and time when the entry expired or will expire.
+
+=attr expires
+
+Boolean value indicating whether or not an entry is expired.
+
+=attr usage_count
+
+The number of times an entry has been used, which typically means how many times the B<Password> string has
+been accessed.
+
+=attr location_changed
+
+Date and time when the entry was last moved to a different parent group.
+
 =cut
 =cut
This page took 0.034509 seconds and 4 git commands to generate.