package HTML::FillInForm;
our $VERSION = '2.22';
use integer; # no floating point math so far!
use strict; # and no funny business, either.
use Carp; # generate better errors with more context
# required for UNIVERSAL->can
require 5.005;
use vars qw($VERSION @ISA);
$VERSION = '2.21';
sub new {
my $class = shift;
my $self = bless {}, $class;
# required for attr_encoded
my %arg = @_ || ();
my $parser_class = $arg{parser_class} || 'HTML::Parser';
eval "require $parser_class;" || die "require $parser_class failed: $@";
@ISA = ($parser_class);
$self->init(@_);
unless ($self->can('attr_encoded')) {
die "attr_encoded method is missing. If are using HTML::Parser, you need at least version 3.26";
}
# tell HTML::Parser not to decode attributes
$self->attr_encoded(1);
return $self;
}
# a few shortcuts to fill()
sub fill_file { my $self = shift; return $self->fill('file' ,@_); }
sub fill_arrayref { my $self = shift; return $self->fill('arrayref' ,@_); }
sub fill_scalarref { my $self = shift; return $self->fill('scalarref',@_); }
# track the keys we support. Useful for file-name detection.
sub _known_keys {
return {
scalarref => 1,
arrayref => 1,
fdat => 1,
fobject => 1,
file => 1,
target => 1,
fill_password => 1,
ignore_fields => 1,
disable_fields => 1,
invalid_fields => 1,
invalid_class => 1,
}
}
sub fill {
my $self = shift;
# If we are called as a class method, go ahead and call new().
$self = $self->new if (not ref $self);
my %option;
# If the first arg is a scalarref, translate that to scalarref => $first_arg
if (ref $_[0] eq 'SCALAR') {
$option{scalarref} = shift;
}
elsif (ref $_[0] eq 'ARRAY') {
$option{arrayref} = shift;
}
elsif (ref $_[0] eq 'GLOB') {
$option{file} = shift;
}
elsif (ref $_[0]) {
croak "data source is not a reference type we understand";
}
# Last chance, if the first arg isn't one of the known keys, we
# assume it is a file name.
elsif (not _known_keys()->{$_[0]} ) {
$option{file} = shift;
}
else {
# Should be a known key. Nothing to do.
}
# Now, check to see if the next arg is also a reference.
my $data;
if (ref $_[0]) {
$data = shift;
$data = [$data] unless ref $data eq 'ARRAY';
for my $source (@$data) {
if (ref $source eq 'HASH') {
push @{ $option{fdat} }, $source;
}
elsif (ref $source) {
if ($source->can('param')) {
push @{ $option{fobject} }, $source;
}
else {
croak "data source $source does not supply a param method";
}
}
elsif (defined $source) {
croak "data source $source is not a hash or object reference";
}
}
}
# load in the rest of the options
%option = (%option, @_);
# As suggested in the docs, merge multiple fdats into one.
if (ref $option{fdat} eq 'ARRAY') {
my %merged;
for my $hash (@{ $option{fdat} }) {
for my $key (keys %$hash) {
$merged{$key} = $hash->{$key};
}
}
$option{'fdat'} = \%merged;
}
my %ignore_fields;
%ignore_fields = map { $_ => 1 } ( ref $option{'ignore_fields'} eq 'ARRAY' )
? @{ $option{ignore_fields} } : $option{ignore_fields} if exists( $option{ignore_fields} );
$self->{ignore_fields} = \%ignore_fields;
my %disable_fields;
%disable_fields = map { $_ => 1 } ( ref $option{'disable_fields'} eq 'ARRAY' )
? @{ $option{disable_fields} } : $option{disable_fields} if exists( $option{disable_fields} );
$self->{disable_fields} = \%disable_fields;
my %invalid_fields;
%invalid_fields = map { $_ => 1 } ( ref $option{'invalid_fields'} eq 'ARRAY' )
? @{ $option{invalid_fields} } : $option{invalid_fields} if exists( $option{invalid_fields} );
$self->{invalid_fields} = \%invalid_fields;
if (my $fdat = $option{fdat}){
# Copy the structure to prevent side-effects.
my %copy;
keys %$fdat; # reset fdat if each or Dumper was called on fdat
while(my($key, $val) = each %$fdat) {
next if exists $ignore_fields{$key};
$copy{ $key } = ref $val eq 'ARRAY' ? [ @$val ] : $val;
}
$self->{fdat} = \%copy;
}
# We want the reference to these objects to go out of scope at the
# end of the method.
local $self->{objects} = [];
if(my $objects = $option{fobject}){
unless(ref($objects) eq 'ARRAY'){
$objects = [ $objects ];
}
for my $object (@$objects){
# make sure objects in 'param_object' parameter support param()
defined($object->can('param')) or
croak("HTML::FillInForm->fill called with fobject option, containing object of type " . ref($object) . " which lacks a param() method!");
}
$self->{objects} = $objects;
}
if (my $target = $option{target}){
$self->{'target'} = $target;
}
if (my $invalid_class = $option{invalid_class}){
$self->{'invalid_class'} = $invalid_class;
} else {
$self->{'invalid_class'} = 'invalid';
}
if (defined($option{fill_password})){
$self->{fill_password} = $option{fill_password};
} else {
$self->{fill_password} = 1;
}
$self->{clear_absent_checkboxes} = $option{clear_absent_checkboxes};
# make sure method has data to fill in HTML form with!
unless(exists $self->{fdat} || $self->{objects}){
croak("HTML::FillInForm->fillInForm() called without 'fobject' or 'fdat' parameter set");
}
local $self->{object_param_cache};
if(my $file = $option{file}){
$self->parse_file($file);
} elsif (my $scalarref = $option{scalarref}){
$self->parse($$scalarref);
} elsif (my $arrayref = $option{arrayref}){
for (@$arrayref){
$self->parse($_);
}
}
$self->eof;
return delete $self->{output};
}
# handles opening HTML tags such as
sub start {
my ($self, $tagname, $attr, $attrseq, $origtext) = @_;
# set the current form
if ($tagname eq 'form') {
$self->{object_param_cache} = {};
if (exists $attr->{'name'} || exists $attr->{'id'}) {
$self->{'current_form'} = $attr->{'name'} || $attr->{'id'};
} else {
# in case of previous one without
delete $self->{'current_form'};
}
}
# This form is not my target.
if (exists $self->{'target'} &&
(! exists $self->{'current_form'} ||
$self->{'current_form'} ne $self->{'target'})) {
$self->{'output'} .= $origtext;
return;
}
# HTML::Parser converts tagname to lowercase, so we don't need /i
if ($self->{option_no_value}) {
$self->{output} .= '>';
delete $self->{option_no_value};
}
# Check if we need to disable this field
$attr->{disabled} = 'disabled'
if exists $attr->{'name'} and
exists $self->{disable_fields}{ $attr->{'name'} } and
$self->{disable_fields}{ $attr->{'name'} } and
not ( exists $attr->{disabled} and $attr->{disabled} );
# Check if we need to invalidate this field
my $invalidating = 0;
if (exists $attr->{name} and
exists $self->{invalid_fields}{ $attr->{name} } and
$self->{invalid_fields}{ $attr->{name} }) {
$invalidating = 1;
if (exists $attr->{class} and length $attr->{class}) {
# don't add the class if it's already there
unless ($attr->{class} =~ /\b\Q$self->{invalid_class}\E\b/) {
$attr->{class} .= " $self->{invalid_class}";
}
} else {
$attr->{class} = $self->{invalid_class};
}
}
if ($tagname eq 'input'){
my $value = exists $attr->{'name'} ? $self->_get_param($attr->{'name'}) : undef;
# force hidden fields to have a value
$value = '' if exists($attr->{'type'}) && $attr->{'type'} eq 'hidden' && ! exists $attr->{'value'} && ! defined $value;
# browsers do not pass unchecked checkboxes at all, so hack around
$value = '' if $self->{clear_absent_checkboxes} && !defined $value && exists($attr->{'type'}) && ($attr->{'type'} eq 'checkbox' || $attr->{'type'} eq 'radio');
if (defined($value)){
# check for input type, noting that default type is text
if (!exists $attr->{'type'} ||
$attr->{'type'} =~ /^(text|textfield|hidden|tel|search|url|email|datetime|date|month|week|time|datetime\-local|number|range|color|)$/i){
if ( ref($value) eq 'ARRAY' ) {
$value = shift @$value;
$value = '' unless defined $value;
}
$attr->{'value'} = __escapeHTML($value);
} elsif (lc $attr->{'type'} eq 'password' && $self->{fill_password}) {
if ( ref($value) eq 'ARRAY' ) {
$value = shift @$value;
$value = '' unless defined $value;
}
$attr->{'value'} = __escapeHTML($value);
} elsif (lc $attr->{'type'} eq 'radio'){
if ( ref($value) eq 'ARRAY' ) {
$value = $value->[0];
$value = '' unless defined $value;
}
# value for radio boxes default to 'on', works with netscape
$attr->{'value'} = 'on' unless exists $attr->{'value'};
if ($attr->{'value'} eq __escapeHTML($value)){
$attr->{'checked'} = 'checked';
} else {
delete $attr->{'checked'};
}
} elsif (lc $attr->{'type'} eq 'checkbox'){
# value for checkboxes default to 'on', works with netscape
$attr->{'value'} = 'on' unless exists $attr->{'value'};
delete $attr->{'checked'}; # Everything is unchecked to start
$value = [ $value ] unless ref($value) eq 'ARRAY';
foreach my $v ( @$value ) {
if ( $attr->{'value'} eq __escapeHTML($v) ) {
$attr->{'checked'} = 'checked';
}
}
# } else {
# warn(qq(Input field of unknown type "$attr->{type}": $origtext));
}
}
$self->{output} .= "<$tagname";
while (my ($key, $value) = each %$attr) {
next if $key eq '/';
$self->{output} .= sprintf qq( %s="%s"), $key, $value;
}
# extra space put here to work around Opera 6.01/6.02 bug
$self->{output} .= ' /' if $attr->{'/'};
$self->{output} .= ">";
} elsif ($tagname eq 'option'){
my $value = defined($self->{selectName}) ? $self->_get_param($self->{selectName}) : undef;
# browsers do not pass selects with no selected options at all,
# so hack around
$value = '' if $self->{clear_absent_checkboxes} && !defined $value;
$value = [ $value ] unless ( ref($value) eq 'ARRAY' );
if ( defined $value->[0] ){
delete $attr->{selected} if exists $attr->{selected};
if(defined($attr->{'value'})){
# option tag has value attr -
if ($self->{selectMultiple}){
# check if the option tag belongs to a multiple option select
foreach my $v ( grep { defined } @$value ) {
if ( $attr->{'value'} eq __escapeHTML($v) ){
$attr->{selected} = 'selected';
}
}
} else {
# if not every value of a fdat ARRAY belongs to a different select tag
if (not $self->{selectSelected}){
if ( $attr->{'value'} eq __escapeHTML($value->[0])){
shift @$value if ref($value) eq 'ARRAY';
$attr->{selected} = 'selected';
$self->{selectSelected} = 1; # remember that an option tag is selected for this select tag
}
}
}
} else {
# option tag has no value attr -
# save for processing under text handler
$self->{option_no_value} = __escapeHTML($value);
}
}
$self->{output} .= "<$tagname";
while (my ($key, $value) = each %$attr) {
$self->{output} .= sprintf qq( %s="%s"), $key, $value;
}
unless ($self->{option_no_value}){
# we can close option tag here
$self->{output} .= ">";
}
} elsif ($tagname eq 'textarea'){
# need to re-output the