]> 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 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 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 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
 
 
 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);
 
 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>.
 =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
 
 
 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
 
 
 =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' },
     }
 
         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
 
 =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 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
 
 =attr notes
 
-Alias for the C<Notes> string value.
+Alias for the B<Notes> string value.
 
 =attr password
 
 
 =attr password
 
-Alias for the C<Password> string value.
+Alias for the B<Password> string value.
 
 =attr title
 
 
 =attr title
 
-Alias for the C<Title> string value.
+Alias for the B<Title> string value.
 
 =attr url
 
 
 =attr url
 
-Alias for the C<URL> string value.
+Alias for the B<URL> string value.
 
 =attr username
 
 
 =attr username
 
-Aliases for the C<UserName> string value.
+Aliases for the B<UserName> string value.
 
 =cut
 
 
 =cut
 
@@ -169,39 +138,42 @@ sub uuid {
         for my $entry (@{$self->history}) {
             $entry->{uuid} = $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};
 }
 
     }
     $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',
 my %ATTRS_STRINGS = (
     title                   => 'Title',
     username                => 'UserName',
@@ -209,33 +181,16 @@ my %ATTRS_STRINGS = (
     url                     => 'URL',
     notes                   => 'Notes',
 );
     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, @_) };
 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;
     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 {
 }
 
 sub init {
@@ -323,9 +278,15 @@ sub _protect {
 
 =method string_value
 
 
 =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
 
 
 =cut
 
@@ -335,17 +296,17 @@ sub string_value {
     return $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>.
 
 
 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
 
 
 =cut
 
@@ -363,7 +324,7 @@ sub _expand_placeholder {
     }
     return if !defined $File::KDBX::PLACEHOLDERS{$placeholder_key};
 
     }
     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 {
     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;
 }
 
     return $str;
 }
 
-sub expanded_string_value {
+sub expand_string_value {
     my $self = shift;
     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);
 }
 
     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;
 =method other_strings
 
     $other = $entry->other_strings;
@@ -416,53 +398,135 @@ sub other_strings {
     return join($delim, @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 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;
 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;
 
 sub binary_value {
     my $self = shift;
-    my $binary = $self->binary_novivify(@_) // return undef;
+    my $binary = $self->binary(@_) // return undef;
     return $binary->{value};
 }
 
     return $binary->{value};
 }
 
-sub auto_type_enabled {
-    my $entry = shift;
-    # TODO
-}
-
 ##############################################################################
 
 =method hmac_otp
 ##############################################################################
 
 =method hmac_otp
@@ -741,8 +805,10 @@ sub size {
 sub history {
     my $self = shift;
     my $entries = $self->{history} //= [];
 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;
 }
 
     return $entries;
 }
 
@@ -761,14 +827,15 @@ sub history_size {
 
 =method prune_history
 
 
 =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)
 
 =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
 
 
 =cut
 
@@ -776,53 +843,198 @@ sub prune_history {
     my $self = shift;
     my %args = @_;
 
     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 $history = $self->history;
 
+    my @removed;
+
     if (0 <= $max_items && $max_items < @$history) {
     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) {
     }
 
     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;
         }
     }
             $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($_) } @_;
 }
 
     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;
     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;
 }
 
 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__
 
 
 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
 =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
 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.
 
 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 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
 =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
 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
 
 * ☑ 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
 
 
 =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<{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}>
 * ☒ 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
 
 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
 
     $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
 
 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
 
 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.039046 seconds and 4 git commands to generate.