]> Dogcows Code - chaz/p5-File-KDBX/blobdiff - lib/File/KDBX/Entry.pm
Remove min_version and clean up a lot of pod
[chaz/p5-File-KDBX] / lib / File / KDBX / Entry.pm
index c4c67b7f73dc335d0449e15a667a332e4ea5f9f3..e97e30a7228d7b8e0d6e9ef0995637331d3d93a3 100644 (file)
@@ -4,21 +4,22 @@ package File::KDBX::Entry;
 use warnings;
 use strict;
 
-use Crypt::Misc 0.029 qw(encode_b32r decode_b64);
+use Crypt::Misc 0.029 qw(decode_b64 encode_b32r);
 use Devel::GlobalDestruction;
 use Encode qw(encode);
 use File::KDBX::Constants qw(:history :icon);
 use File::KDBX::Error;
-use File::KDBX::Util qw(:function :uri generate_uuid load_optional);
-use List::Util qw(sum0);
-use Ref::Util qw(is_plain_hashref is_ref);
-use Scalar::Util qw(looks_like_number refaddr);
+use File::KDBX::Util qw(:assert :class :coercion :erase :function :uri generate_uuid load_optional);
+use Hash::Util::FieldHash;
+use List::Util qw(first sum0);
+use Ref::Util qw(is_coderef is_hashref is_plain_hashref);
+use Scalar::Util qw(blessed looks_like_number);
 use Storable qw(dclone);
 use Time::Piece;
 use boolean;
 use namespace::clean;
 
-use parent 'File::KDBX::Object';
+extends 'File::KDBX::Object';
 
 our $VERSION = '999.999'; # VERSION
 
@@ -26,20 +27,6 @@ my $PLACEHOLDER_MAX_DEPTH = 10;
 my %PLACEHOLDERS;
 my %STANDARD_STRINGS = map { $_ => 1 } qw(Title UserName Password URL Notes);
 
-sub _parent_container { 'entries' }
-
-=attr uuid
-
-128-bit UUID identifying the entry within the 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 database.
-
 =attr foreground_color
 
 Text color represented as a string of the form C<#000000>.
@@ -52,29 +39,28 @@ Background color represented as a string of the form C<#FFFFFF>.
 
 TODO
 
-=attr tags
+=attr auto_type_enabled
+
+Whether or not the entry is eligible to be matched for auto-typing.
+
+=attr auto_type_obfuscation
+
+Whether or not to use some kind of obfuscation when sending keystroke sequences to applications.
+
+=attr auto_type_default_sequence
 
-Text string with arbitrary tags which can be used to build a taxonomy.
+The default auto-type keystroke sequence.
 
-=attr auto_type
+=attr auto_type_associations
 
-Auto-type details.
+An array of window title / keystroke sequence associations.
 
     {
-        enabled                     => true,
-        data_transfer_obfuscation   => 0,
-        default_sequence            => '{USERNAME}{TAB}{PASSWORD}{ENTER}',
-        associations                => [
-            {
-                window              => 'My Bank - Mozilla Firefox',
-                keystroke_sequence  => '{PASSWORD}{ENTER}',
-            },
-        ],
+        window              => 'Example Window Title',
+        keystroke_sequence  => '{USERNAME}{TAB}{PASSWORD}{ENTER}',
     }
 
-=attr previous_parent_group
-
-128-bit UUID identifying a group within the database.
+Keystroke sequences can have </Placeholders>, most commonly C<{USERNAME}> and C<{PASSWORD}>.
 
 =attr quality_check
 
@@ -95,68 +81,51 @@ Hash with entry strings, including the standard strings as well as any custom on
         MySystem => { value => 'The mainframe' },
     }
 
+There are methods available to provide more convenient access to strings, including L</string>,
+L</string_value>, L</expand_string_value> and L</string_peek>.
+
 =attr binaries
 
-Files or attachments.
+Files or attachments. Binaries are similar to strings except they have a value of bytes instead of test
+characters.
 
-=attr custom_data
+    {
+        'myfile.txt'    => {
+            value   => '...',
+        },
+        'mysecrets.txt' => {
+            value   => '...',
+            protect => true,
+        },
+    }
 
-A set of key-value pairs used to store arbitrary data, usually used by software to keep track of state rather
-than by end users (who typically work with the strings and binaries).
+There are methods available to provide more convenient access to binaries, including L</binary> and
+L</binary_value>.
 
 =attr history
 
 Array of historical entries. Historical entries are prior versions of the same entry so they all share the
 same UUID with the current entry.
 
-=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 C<Password> string has
-been accessed.
-
-=attr location_changed
-
-Date and time when the entry was last moved to a different group.
-
 =attr notes
 
-Alias for the C<Notes> string value.
+Alias for the B<Notes> string value.
 
 =attr password
 
-Alias for the C<Password> string value.
+Alias for the B<Password> string value.
 
 =attr title
 
-Alias for the C<Title> string value.
+Alias for the B<Title> string value.
 
 =attr url
 
-Alias for the C<URL> string value.
+Alias for the B<URL> string value.
 
 =attr username
 
-Aliases for the C<UserName> string value.
+Aliases for the B<UserName> string value.
 
 =cut
 
@@ -169,39 +138,42 @@ sub uuid {
         for my $entry (@{$self->history}) {
             $entry->{uuid} = $uuid;
         }
-        # if (defined $old_uuid and my $kdbx = $KDBX{refaddr($self)}) {
-        #     $kdbx->_update_entry_uuid($old_uuid, $uuid, $self);
-        # }
+        $self->_signal('uuid.changed', $uuid, $old_uuid) if defined $old_uuid && $self->is_current;
     }
     $self->{uuid};
 }
 
-my @ATTRS = qw(uuid custom_data history);
-my %ATTRS = (
-    # uuid                    => sub { generate_uuid(printable => 1) },
-    icon_id                 => ICON_PASSWORD,
-    custom_icon_uuid        => undef,
-    foreground_color        => '',
-    background_color        => '',
-    override_url            => '',
-    tags                    => '',
-    auto_type               => sub { +{} },
-    previous_parent_group   => undef,
-    quality_check           => true,
-    strings                 => sub { +{} },
-    binaries                => sub { +{} },
-    # custom_data             => sub { +{} },
-    # history                 => sub { +[] },
-);
-my %ATTRS_TIMES = (
-    last_modification_time  => sub { gmtime },
-    creation_time           => sub { gmtime },
-    last_access_time        => sub { gmtime },
-    expiry_time             => sub { gmtime },
-    expires                 => false,
-    usage_count             => 0,
-    location_changed        => sub { gmtime },
-);
+# has uuid                    => sub { generate_uuid(printable => 1) };
+has icon_id                 => ICON_PASSWORD,   coerce => \&to_icon_constant;
+has custom_icon_uuid        => undef,           coerce => \&to_uuid;
+has foreground_color        => '',              coerce => \&to_string;
+has background_color        => '',              coerce => \&to_string;
+has override_url            => '',              coerce => \&to_string;
+has tags                    => '',              coerce => \&to_string;
+has auto_type               => {};
+has previous_parent_group   => undef,           coerce => \&to_uuid;
+has quality_check           => true,            coerce => \&to_bool;
+has strings                 => {};
+has binaries                => {};
+has times                   => {};
+# has custom_data             => {};
+# has history                 => [];
+
+has last_modification_time  => sub { gmtime }, store => 'times', coerce => \&to_time;
+has creation_time           => sub { gmtime }, store => 'times', coerce => \&to_time;
+has last_access_time        => sub { gmtime }, store => 'times', coerce => \&to_time;
+has expiry_time             => sub { gmtime }, store => 'times', coerce => \&to_time;
+has expires                 => false,          store => 'times', coerce => \&to_bool;
+has usage_count             => 0,              store => 'times', coerce => \&to_number;
+has location_changed        => sub { gmtime }, store => 'times', coerce => \&to_time;
+
+# has 'auto_type.auto_type_enabled'                   => true, coerce => \&to_bool;
+has 'auto_type_obfuscation' => 0, path => 'auto_type.data_transfer_obfuscation',
+    coerce => \&to_number;
+has 'auto_type_default_sequence'          => '{USERNAME}{TAB}{PASSWORD}{ENTER}',
+    path => 'auto_type.default_sequence', coerce => \&to_string;
+has 'auto_type_associations'              => [], path => 'auto_type.associations';
+
 my %ATTRS_STRINGS = (
     title                   => 'Title',
     username                => 'UserName',
@@ -209,33 +181,16 @@ my %ATTRS_STRINGS = (
     url                     => 'URL',
     notes                   => 'Notes',
 );
-
-while (my ($attr, $default) = each %ATTRS) {
-    no strict 'refs'; ## no critic (ProhibitNoStrict)
-    *{$attr} = sub {
-        my $self = shift;
-        $self->{$attr} = shift if @_;
-        $self->{$attr} //= (ref $default eq 'CODE') ? $default->($self) : $default;
-    };
-}
-while (my ($attr, $default) = each %ATTRS_TIMES) {
-    no strict 'refs'; ## no critic (ProhibitNoStrict)
-    *{$attr} = sub {
-        my $self = shift;
-        $self->{times} //= {};
-        $self->{times}{$attr} = shift if @_;
-        $self->{times}{$attr} //= (ref $default eq 'CODE') ? $default->($self) : $default;
-    };
-}
 while (my ($attr, $string_key) = each %ATTRS_STRINGS) {
     no strict 'refs'; ## no critic (ProhibitNoStrict)
     *{$attr} = sub { shift->string_value($string_key, @_) };
-    *{"expanded_${attr}"} = sub { shift->expanded_string_value($string_key, @_) };
+    *{"expand_${attr}"} = sub { shift->expand_string_value($string_key, @_) };
 }
 
-sub _set_default_attributes {
+my @ATTRS = qw(uuid custom_data history auto_type_enabled);
+sub _set_nonlazy_attributes {
     my $self = shift;
-    $self->$_ for @ATTRS, keys %ATTRS, keys %ATTRS_TIMES, keys %ATTRS_STRINGS;
+    $self->$_ for @ATTRS, keys %ATTRS_STRINGS, list_attributes(ref $self);
 }
 
 sub init {
@@ -323,9 +278,15 @@ sub _protect {
 
 =method string_value
 
-    $string = $entry->string_value;
+    $string = $entry->string_value($string_key);
+
+Access a string value directly. The arguments are the same as for L</string>. Returns C<undef> if the string
+is not set or is currently memory-protected. This is just a shortcut for:
 
-Access a string value directly. Returns C<undef> if the string is not set.
+    my $string = do {
+        my $s = $entry->string(...);
+        defined $s ? $s->{value} : undef;
+    };
 
 =cut
 
@@ -335,17 +296,17 @@ sub string_value {
     return $string->{value};
 }
 
-=method expanded_string_value
+=method expand_string_value
 
-    $string = $entry->expanded_string_value;
+    $string = $entry->expand_string_value;
 
 Same as L</string_value> but will substitute placeholders and resolve field references. Any placeholders that
 do not expand to values are left as-is.
 
 See L</Placeholders>.
 
-Some placeholders (notably field references) require the entry be associated with a database and will throw an
-error if there is no association.
+Some placeholders (notably field references) require the entry be connected to a database and will throw an
+error if it is not.
 
 =cut
 
@@ -363,7 +324,7 @@ sub _expand_placeholder {
     }
     return if !defined $File::KDBX::PLACEHOLDERS{$placeholder_key};
 
-    my $local_key = join('/', refaddr($self), $placeholder_key);
+    my $local_key = join('/', Hash::Util::FieldHash::id($self), $placeholder_key);
     local $PLACEHOLDERS{$local_key} = my $handler = $PLACEHOLDERS{$local_key} // do {
         my $handler = $File::KDBX::PLACEHOLDERS{$placeholder_key} or next;
         memoize recurse_limit($handler, $PLACEHOLDER_MAX_DEPTH, sub {
@@ -392,12 +353,33 @@ sub _expand_string {
     return $str;
 }
 
-sub expanded_string_value {
+sub expand_string_value {
     my $self = shift;
-    my $str  = $self->string_value(@_) // return undef;
+    my $str  = $self->string_peek(@_) // return undef;
+    my $cleanup = erase_scoped $str;
     return $self->_expand_string($str);
 }
 
+=attr expand_notes
+
+Shortcut equivalent to C<< ->expand_string_value('Notes') >>.
+
+=attr expand_password
+
+Shortcut equivalent to C<< ->expand_string_value('Password') >>.
+
+=attr expand_title
+
+Shortcut equivalent to C<< ->expand_string_value('Title') >>.
+
+=attr expand_url
+
+Shortcut equivalent to C<< ->expand_string_value('URL') >>.
+
+=attr expand_username
+
+Shortcut equivalent to C<< ->expand_string_value('UserName') >>.
+
 =method other_strings
 
     $other = $entry->other_strings;
@@ -416,53 +398,135 @@ sub other_strings {
     return join($delim, @strings);
 }
 
+=method string_peek
+
+    $string = $entry->string_peek($string_key);
+
+Same as L</string_value> but can also retrieve the value from protected-memory if the value is currently
+protected.
+
+=cut
+
 sub string_peek {
     my $self = shift;
     my $string = $self->string(@_);
     return defined $string->{value} ? $string->{value} : $self->kdbx->peek($string);
 }
 
-sub password_peek { $_[0]->string_peek('Password') }
+##############################################################################
+
+=method add_auto_type_association
+
+    $entry->add_auto_type_association(\%association);
+
+Add a new auto-type association to an entry.
+
+=cut
+
+sub add_auto_type_association {
+    my $self        = shift;
+    my $association = shift;
+    push @{$self->auto_type_associations}, $association;
+}
+
+=method expand_keystroke_sequence
+
+    $string = $entry->expand_keystroke_sequence($keystroke_sequence);
+    $string = $entry->expand_keystroke_sequence(\%association);
+    $string = $entry->expand_keystroke_sequence;    # use default auto-type sequence
+
+Get a keystroke sequence after placeholder expansion.
+
+=cut
+
+sub expand_keystroke_sequence {
+    my $self = shift;
+    my $association = shift;
+
+    my $keys;
+    if ($association) {
+        $keys = is_hashref($association) && exists $association->{keystroke_sequence} ?
+        $association->{keystroke_sequence} : defined $association ? $association : '';
+    }
+
+    $keys = $self->auto_type_default_sequence if !$keys;
+    # TODO - Fall back to getting default sequence from parent group, which probably means we shouldn't be
+    # setting a default value in the entry..
+
+    return $self->_expand_string($keys);
+}
 
 ##############################################################################
 
+=method binary
+
+    \%binary = $entry->binary($binary_key);
+
+    $entry->binary($binary_key, \%binary);
+    $entry->binary($binary_key, %attributes);
+    $entry->binary($binary_key, $value); # same as: value => $value
+
+Get or set a binary. Every binary has a unique (to the entry) key and flags and so are returned as a hash
+structure. For example:
+
+    $binary = {
+        value   => '...',
+        protect => true,    # optional
+    };
+
+Every binary should have a value (but might be C<undef> due to memory protection) and these optional flags
+which might exist:
+
+=for :list
+* C<protect> - Whether or not the binary value should be memory-protected.
+
+=cut
+
 sub binary {
     my $self = shift;
-    my $key  = shift or throw 'Must provide a binary key to access';
-    if (@_) {
-        my $arg = @_ == 1 ? shift : undef;
-        my %args;
-        @args{keys %$arg} = values %$arg if ref $arg eq 'HASH';
-        $args{value} = $arg if !ref $arg;
-        while (my ($field, $value) = each %args) {
-            $self->{binaries}{$key}{$field} = $value;
+    my %args = @_     == 2 ? (key => shift, value => shift)
+             : @_ % 2 == 1 ? (key => shift, @_) : @_;
+
+    if (!defined $args{key} && !defined $args{value}) {
+        my %standard = (value => 1, protect => 1);
+        my @other_keys = grep { !$standard{$_} } keys %args;
+        if (@other_keys == 1) {
+            my $key = $args{key} = $other_keys[0];
+            $args{value} = delete $args{$key};
         }
     }
-    my $binary = $self->{binaries}{$key} //= {value => ''};
-    if (defined (my $ref = $binary->{ref})) {
-        $binary = $self->{binaries}{$key} = dclone($self->kdbx->binaries->{$ref});
+
+    my $key = delete $args{key} or throw 'Must provide a binary key to access';
+
+    return $self->{binaries}{$key} = $args{value} if is_plain_hashref($args{value});
+
+    assert { !defined $args{value} || !utf8::is_utf8($args{value}) };
+    while (my ($field, $value) = each %args) {
+        $self->{binaries}{$key}{$field} = $value;
     }
-    return $binary;
+    return $self->{binaries}{$key};
 }
 
-sub binary_novivify {
-    my $self = shift;
-    my $binary_key = shift;
-    return if !$self->{binaries}{$binary_key} && !@_;
-    return $self->binary($binary_key, @_);
-}
+=method binary_value
+
+    $binary = $entry->binary_value($binary_key);
+
+Access a binary value directly. The arguments are the same as for L</binary>. Returns C<undef> if the binary
+is not set or is currently memory-protected. This is just a shortcut for:
+
+    my $binary = do {
+        my $b = $entry->binary(...);
+        defined $b ? $b->{value} : undef;
+    };
+
+=cut
 
 sub binary_value {
     my $self = shift;
-    my $binary = $self->binary_novivify(@_) // return undef;
+    my $binary = $self->binary(@_) // return undef;
     return $binary->{value};
 }
 
-sub auto_type_enabled {
-    my $entry = shift;
-    # TODO
-}
-
 ##############################################################################
 
 =method hmac_otp
@@ -741,8 +805,10 @@ sub size {
 sub history {
     my $self = shift;
     my $entries = $self->{history} //= [];
-    # FIXME - Looping through entries on each access is too expensive.
-    @$entries = map { $self->_wrap_entry($_, $self->kdbx) } @$entries;
+    if (@$entries && !blessed($entries->[0])) {
+        @$entries = map { $self->_wrap_entry($_, $self->kdbx) } @$entries;
+    }
+    assert { !any { !blessed $_ } @$entries };
     return $entries;
 }
 
@@ -761,14 +827,15 @@ sub history_size {
 
 =method prune_history
 
-    $entry->prune_history(%options);
+    @removed_historical_entries = $entry->prune_history(%options);
 
-Remove as many older historical entries as necessary to get under the database limits. The limits are taken
-from the associated database (if any) or can be overridden with C<%options>:
+Remove just as many older historical entries as necessary to get under the database limits. The limits are
+taken from the connected database (if any) or can be overridden with C<%options>:
 
 =for :list
 * C<max_items> - Maximum number of historical entries to keep (default: 10, no limit: -1)
 * C<max_size> - Maximum total size (in bytes) of historical entries to keep (default: 6 MiB, no limit: -1)
+* C<max_age> - Maximum age (in days) of historical entries to keep (default: 365, no limit: -1)
 
 =cut
 
@@ -776,53 +843,198 @@ sub prune_history {
     my $self = shift;
     my %args = @_;
 
-    my $max_items = $args{max_items} // eval { $self->kdbx->history_max_items }
-        // HISTORY_DEFAULT_MAX_ITEMS;
-    my $max_size  = $args{max_size} // eval { $self->kdbx->history_max_size }
-        // HISTORY_DEFAULT_MAX_SIZE;
+    my $max_items = $args{max_items} // eval { $self->kdbx->history_max_items } // HISTORY_DEFAULT_MAX_ITEMS;
+    my $max_size  = $args{max_size}  // eval { $self->kdbx->history_max_size }  // HISTORY_DEFAULT_MAX_SIZE;
+    my $max_age   = $args{max_age}   // HISTORY_DEFAULT_MAX_AGE;
 
-    # history is ordered oldest to youngest
+    # history is ordered oldest to newest
     my $history = $self->history;
 
+    my @removed;
+
     if (0 <= $max_items && $max_items < @$history) {
-        splice @$history, -$max_items;
+        push @removed, splice @$history, -$max_items;
     }
 
     if (0 <= $max_size) {
         my $current_size = $self->history_size;
         while ($max_size < $current_size) {
-            my $entry = shift @$history;
+            push @removed, my $entry = shift @$history;
             $current_size -= $entry->size;
         }
     }
+
+    if (0 <= $max_age) {
+        my $cutoff = gmtime - ($max_age * 86400);
+        for (my $i = @$history - 1; 0 <= $i; --$i) {
+            my $entry = $history->[$i];
+            next if $cutoff <= $entry->last_modification_time;
+            push @removed, splice @$history, $i, 1;
+        }
+    }
+
+    @removed = sort { $a->last_modification_time <=> $b->last_modification_time } @removed;
+    return @removed;
 }
 
-sub add_history {
+=method add_historical_entry
+
+    $entry->add_historical_entry($entry);
+
+Add an entry to the history.
+
+=cut
+
+sub add_historical_entry {
     my $self = shift;
     delete $_->{history} for @_;
     push @{$self->{history} //= []}, map { $self->_wrap_entry($_) } @_;
 }
 
+=method remove_historical_entry
+
+    $entry->remove_historical_entry($historical_entry);
+
+Remove an entry from the history.
+
+=cut
+
+sub remove_historical_entry {
+    my $self    = shift;
+    my $entry   = shift;
+    my $history = $self->history;
+
+    my @removed;
+    for (my $i = @$history - 1; 0 <= $i; --$i) {
+        my $item = $history->[$i];
+        next if Hash::Util::FieldHash::id($entry) != Hash::Util::FieldHash::id($item);
+        push @removed, splice @{$self->{history}}, $i, 1;
+    }
+    return @removed;
+}
+
+=method current_entry
+
+    $current_entry = $entry->current_entry;
+
+Get an entry's current entry. If the entry itself is current (not historical), itself is returned.
+
+=cut
+
+sub current_entry {
+    my $self    = shift;
+    my $parent  = $self->group;
+
+    if ($parent) {
+        my $id = $self->uuid;
+        my $entry = first { $id eq $_->uuid } @{$parent->entries};
+        return $entry if $entry;
+    }
+
+    return $self;
+}
+
+=method is_current
+
+    $bool = $entry->is_current;
+
+Get whether or not an entry is considered current (i.e. not historical). An entry is current if it is directly
+in the parent group's entry list.
+
+=cut
+
+sub is_current {
+    my $self    = shift;
+    my $current = $self->current_entry;
+    return Hash::Util::FieldHash::id($self) == Hash::Util::FieldHash::id($current);
+}
+
+=method is_historical
+
+    $bool = $entry->is_historical;
+
+Get whether or not an entry is considered historical (i.e. not current).
+
+This is just the inverse of L</is_current>.
+
+=cut
+
+sub is_historical { !$_[0]->is_current }
+
+=method remove
+
+    $entry = $entry->remove;
+
+Remove an entry from its parent group. If the entry is historical, remove it from the history of the current
+entry. If the entry is current, this behaves the same as L<File::KDBX::Object/remove>.
+
+=cut
+
+sub remove {
+    my $self    = shift;
+    my $current = $self->current_entry;
+    return $self if $current->remove_historical_entry($self);
+    $self->SUPER::remove(@_);
+}
+
 ##############################################################################
 
-sub begin_work {
+=method searching_enabled
+
+    $bool = $entry->searching_enabled;
+
+Get whether or not an entry may show up in search results. This is determine from the entry's parent group's
+L<File::KDBX::Group/effective_enable_searching> value.
+
+Throws if entry has no parent group or if the entry is not connected to a database.
+
+=cut
+
+sub searching_enabled {
     my $self = shift;
-    require File::KDBX::Transaction;
-    return File::KDBX::Transaction->new($self, @_);
+    my $parent = $self->group;
+    return $parent->effective_enable_searching if $parent;
+    return true;
+}
+
+sub auto_type_enabled {
+    my $self = shift;
+    $self->auto_type->{enabled} = to_bool(shift) if @_;
+    $self->auto_type->{enabled} //= true;
+    return false if !$self->auto_type->{enabled};
+    return true if !$self->is_connected;
+    my $parent = $self->group;
+    return $parent->effective_enable_auto_type if $parent;
+    return true;
+}
+
+##############################################################################
+
+sub _signal {
+    my $self = shift;
+    my $type = shift;
+    return $self->SUPER::_signal("entry.$type", @_);
 }
 
 sub _commit {
     my $self = shift;
-    my $txn  = shift;
-    $self->add_history($txn->original);
-    $self->last_modification_time(gmtime);
+    my $orig = shift;
+    $self->add_historical_entry($orig);
+    my $time = gmtime;
+    $self->last_modification_time($time);
+    $self->last_access_time($time);
 }
 
-sub label { shift->expanded_title(@_) }
+sub label { shift->expand_title(@_) }
+
+### Name of the parent attribute expected to contain the object
+sub _parent_container { 'entries' }
 
 1;
 __END__
 
+=for Pod::Coverage auto_type times
+
 =head1 DESCRIPTION
 
 An entry in a KDBX database is a record that can contains strings (also called "fields") and binaries (also
@@ -830,11 +1042,11 @@ called "files" or "attachments"). Every string and binary has a key or name. The
 that every entry has:
 
 =for :list
-* C<Title>
-* C<UserName>
-* C<Password>
-* C<URL>
-* C<Notes>
+* B<Title>
+* B<UserName>
+* B<Password>
+* B<URL>
+* B<Notes>
 
 Beyond this, you can store any number of other strings and any number of binaries that you can use for
 whatever purpose you want.
@@ -843,12 +1055,14 @@ There is also some metadata associated with an entry. Each entry in a database i
 a UUID. An entry can also have an icon associated with it, and there are various timestamps. Take a look at
 the attributes to see what's available.
 
+A B<File::KDBX::Entry> is a subclass of L<File::KDBX::Object>.
+
 =head2 Placeholders
 
 Entry string and auto-type key sequences can have placeholders or template tags that can be replaced by other
 values. Placeholders can appear like C<{PLACEHOLDER}>. For example, a B<URL> string might have a value of
 C<http://example.com?user={USERNAME}>. C<{USERNAME}> is a placeholder for the value of the B<UserName> string
-of the same entry. If the C<UserName> string had a value of "batman", the B<URL> string would expand to
+of the same entry. If the B<UserName> string had a value of "batman", the B<URL> string would expand to
 C<http://example.com?user=batman>.
 
 Some placeholders take an argument, where the argument follows the tag after a colon but before the closing
@@ -923,7 +1137,7 @@ This software supports many (but not all) of the placeholders documented there.
 * ☑ C<{DT_UTC_MINUTE}> Minute Year component of the current UTC time
 * ☑ C<{DT_UTC_SECOND}> - Second component of the current UTC time
 
-If the current date and time is <2012-07-25 17:05:34>, the "simple" form would be C<20120725170534>.
+If the current date and time is C<2012-07-25 17:05:34>, the "simple" form would be C<20120725170534>.
 
 =head3 Special Key Placeholders
 
@@ -961,7 +1175,7 @@ C<{NUMPAD4}>, C<{NUMPAD5}>, C<{NUMPAD6}>, C<{NUMPAD7}>, C<{NUMPAD8}>, C<{NUMPAD9
 * ☒ C<{CLIPBOARD}>
 * ☒ C<{CMD:/CommandLine/Options/}>
 * ☑ C<{C:Comment}> - Comments are simply replaced by nothing
-* ☑ C<{ENV:} and C<%ENV%> - Environment variables
+* ☑ C<{ENV:}> and C<%ENV%> - Environment variables
 * ☒ C<{GROUP_SEL_NOTES}>
 * ☒ C<{GROUP_SEL_PATH}>
 * ☒ C<{GROUP_SEL}>
@@ -986,7 +1200,7 @@ a placeholder, just set it in the C<%File::KDBX::PLACEHOLDERS> hash. For example
 
 If the placeholder is expanded in the context of an entry, C<$entry> is the B<File::KDBX::Entry> object in
 context. Otherwise it is C<undef>. An entry is in context if, for example, the placeholder is in an entry's
-strings or auto-complete key sequences.
+strings or auto-type key sequences.
 
     $File::KDBX::PLACEHOLDERS{'MY_PLACEHOLDER:'} = sub {
         my ($entry, $arg) = @_;         #    ^ Notice the colon here
@@ -995,7 +1209,7 @@ strings or auto-complete key sequences.
 
 If the name of the placeholder ends in a colon, then it is expected to receive an argument. During expansion,
 everything after the colon and before the end of the placeholder is passed to your placeholder handler
-subroutine. So if the placeholder is C<{MY_PLACEHOLDER:whatever}>, C<$arg> will have the value C<whatever>.
+subroutine. So if the placeholder is C<{MY_PLACEHOLDER:whatever}>, C<$arg> will have the value B<whatever>.
 
 An argument is required for placeholders than take one. I.e. The placeholder handler won't be called if there
 is no argument. If you want a placeholder to support an optional argument, you'll need to set the placeholder
This page took 0.046541 seconds and 4 git commands to generate.