diff --git a/lib/HTML/FormHandler/Field.pm b/lib/HTML/FormHandler/Field.pm
index 5cb963ca..a19c6c0e 100644
--- a/lib/HTML/FormHandler/Field.pm
+++ b/lib/HTML/FormHandler/Field.pm
@@ -173,6 +173,18 @@ See also L.
return $field->add_error( 'bad data' ) if $bad;
+The first argument is the localization FORMAT, which for a
+L handle means bracket notation in it is compiled and
+executed. Do not build that argument out of submitted data: a value
+containing a C<[...]> group would be run as a method call rather than
+shown. Pass the value as an argument instead, where it is inert:
+
+ # wrong -- the submitted value becomes part of the format
+ $field->add_error( "The value '" . $field->value . "' is not allowed" );
+
+ # right -- the format is yours, the value is just an argument
+ $field->add_error( "The value '[_1]' is not allowed", $field->value );
+
=item error_fields
Compound fields will have an array of errors from the subfields.
@@ -1415,7 +1427,21 @@ sub add_error {
unless ( defined $message[0] ) {
@message = ( $class_messages->{field_invalid});
}
- @message = @{$message[0]} if ref $message[0] eq 'ARRAY';
+ if ( ref $message[0] eq 'ARRAY' ) {
+ # An arrayref argument is a value, not a message specification. The
+ # list-or-arrayref spelling here is the same convenience idiom as
+ # add_element_class and friends, it is not documented for add_error,
+ # and nothing in the distribution reaches it -- but request data does:
+ # $field->add_error($field->value), where the request parser folded a
+ # duplicate parameter into an arrayref, puts submitted text in element
+ # 0, which _localize hands to Locale::Maketext as bracket-notation
+ # source. Dereference as before, but render element 0 literally.
+ # A caller who really wants a compiled template passes it as a plain
+ # list: $field->add_error($template, @args).
+ my @args = @{ $message[0] };
+ $args[0] = $self->_escape_bracket_notation( $args[0] );
+ @message = @args;
+ }
my $out;
try {
$out = $self->_localize(@message);
diff --git a/lib/HTML/FormHandler/Field/Date.pm b/lib/HTML/FormHandler/Field/Date.pm
index 11fd5ec1..13da57e4 100644
--- a/lib/HTML/FormHandler/Field/Date.pm
+++ b/lib/HTML/FormHandler/Field/Date.pm
@@ -130,7 +130,11 @@ sub validate {
my $dt = eval { $strp->parse_datetime( $self->value ) };
unless ($dt) {
- $self->add_error( $strp->errmsg || $@ );
+ # The parser's message is not ours to hand to the localizer as a
+ # bracket-notation FORMAT. DateTime::Format::Strptime 1.80 does not
+ # quote the rejected input into errmsg, but that is the parser's text
+ # to change, and the `|| $@` fallback is a second channel.
+ $self->add_error( $self->_escape_bracket_notation( $strp->errmsg || $@ ) );
return;
}
$self->_set_value($dt);
diff --git a/lib/HTML/FormHandler/Validate.pm b/lib/HTML/FormHandler/Validate.pm
index 9d8d3896..0d29bb97 100644
--- a/lib/HTML/FormHandler/Validate.pm
+++ b/lib/HTML/FormHandler/Validate.pm
@@ -164,13 +164,40 @@ sub _build_apply_list {
$self->add_action(@apply_list);
}
+# Locale::Maketext treats its FORMAT argument as bracket-notation source: any
+# '[...]' group inside it is compiled into method-dispatch code (see _compile
+# in Locale::Maketext). Messages FormHandler itself authors are templates on
+# purpose, but three kinds of text reaching _apply_actions are not ours and do
+# embed request data:
+#
+# * warnings trapped by the $SIG{__WARN__} handler in _apply_actions -- Perl
+# quotes the offending value into them verbatim, so a submitted value like
+# '[sprintf,%2000000000d,0]' arrives as a well-formed bracket group;
+# * a type constraint's failure message -- the type system renders a rejected
+# reference in bracket-and-comma form (Devel::PartialDump when Moose can
+# load it, Type::Tiny's own dumper always), so a duplicate request
+# parameter is enough to put '[ "a", "b" ]' into the format;
+# * exceptions from a coercion or a transform.
+#
+# Render those literally instead. Tilde is Locale::Maketext's escape character;
+# text containing no brackets comes back unchanged, so lexicon lookups and
+# translated type-constraint messages behave exactly as before.
+sub _escape_bracket_notation {
+ my ( $self, $text ) = @_;
+
+ return $text if !defined $text || ref $text;
+ $text =~ s/~/~~/g;
+ $text =~ s/([\[\]])/~$1/g;
+ return $text;
+}
+
sub _apply_actions {
my $self = shift;
my $error_message;
local $SIG{__WARN__} = sub {
my $error = shift;
- $error_message = $error;
+ $error_message = $self->_escape_bracket_notation($error);
return 1;
};
@@ -205,10 +232,11 @@ sub _apply_actions {
my $coerce_returned = eval { $tobj->coerce($value) };
if ($@) {
if ( $tobj->has_message ) {
- $error_message = $tobj->message->($value);
+ $error_message = $self->_escape_bracket_notation(
+ $tobj->message->($value) );
}
else {
- $error_message = $@;
+ $error_message = $self->_escape_bracket_notation($@);
}
}
else {
@@ -217,7 +245,8 @@ sub _apply_actions {
}
}
- $error_message ||= $tobj->validate($new_value);
+ $error_message ||= $self->_escape_bracket_notation(
+ $tobj->validate($new_value) );
}
# now maybe: http://search.cpan.org/~rgarcia/perl-5.10.0/pod/perlsyn.pod#Smart_matching_in_detail
# actions in a hashref
@@ -242,7 +271,8 @@ sub _apply_actions {
$action->{transform}->($value, $self);
};
if ($@) {
- $error_message = $@ || $self->get_message('error_occurred');
+ $error_message = $self->_escape_bracket_notation($@)
+ || $self->get_message('error_occurred');
}
else {
$self->_set_value($new_value);