package HTML::DOM::Element::Form;
use strict;
use warnings;
no Carp();
use URI;
require HTML::DOM::Element;
require HTML::DOM::NodeList::Magic;
#require HTML::DOM::Collection::Elements;
our $VERSION = '0.035';
our @ISA = qw'HTML::DOM::Element';
use overload fallback => 1,
'@{}' => sub { shift->elements },
'%{}' => sub {
my $self = shift;
$self->isa(scalar caller) || caller->isa('HTML::TreeBuilder')
and return $self;
$self->elements;
};
my %elem_elems = (
input => 1,
button => 1,
select => 1,
textarea => 1,
);
sub elements {
my $self = shift;
my $collection = $self->{_HTML_DOM_elems} ||= do {
my $collection = HTML::DOM::Collection::Elements->new(
my $list = HTML::DOM::NodeList::Magic->new(
sub {
no warnings 'uninitialized';
grep(
$elem_elems{tag $_} && attr $_ 'type', ne 'image',
$self->descendants
),
@{ $self->{_HTML_DOM_mg_elems}||[] }
}
));
$self->ownerDocument-> _register_magic_node_list($list);
$collection;
};
weaken $self;
if (wantarray) {
@$collection
}
else {
$collection;
}
}
sub add_element { # helper routine that formies use to add themselves to
my $self = shift; # the elements list
push @{ $self->{_HTML_DOM_mg_elems} ||= [] }, shift
if $elem_elems{ $_[0]->tag };
return;
}
sub remove_element { # and this is how formies remove themselves when they
my $self = shift; # get moved around the DOM
my $removee = shift;
@{ $self->{_HTML_DOM_mg_elems} }
= grep $_ != $removee, @{ $self->{_HTML_DOM_elems} ||= [] }
}
sub length { shift->elements -> length }
sub name { no warnings; shift->_attr( name => @_) . '' }
sub acceptCharset { shift->_attr('accept-charset' => @_) }
sub action {
my $self = shift;
(my $base = $self->ownerDocument->base)
or return $self->_attr('action', @_);
(new_abs URI
$self->_attr('action' => @_),
$self->ownerDocument->base)
->as_string
}
sub enctype {
my $ret = shift->_attr('enctype' => @_);
defined $ret ? $ret : 'application/x-www-form-urlencoded'
}
*encoding=*enctype;
sub method {
my $ret = shift->_attr('method' => @_);
defined $ret ? lc $ret : 'get'
}
sub target { shift->_attr('target' => @_) }
sub submit { shift->trigger_event('submit') }
sub reset {
shift->trigger_event('reset');
}
sub trigger_event {
my ($a,$evnt) = (shift,shift);
$a->SUPER::trigger_event(
$evnt,
submit_default =>
$a->ownerDocument->
default_event_handler_for('submit'),
reset_default => sub {
$_->_reset for shift->target->elements
},
@_,
);
}
# ------ HTML::Form compatibility methods ------ #
sub inputs {
my @ret;
my %pos;
my $self = shift;
# This used to use ‘$self->elements’, but ->elements no longer
# includes image buttons.
for(
grep($elem_elems{tag $_}, $self->descendants),
@{ $self->{_HTML_DOM_mg_elems}||[] }
) {
next if (my $tag = tag $_) eq 'button'; # HTML::Form doesn't deal
# with s.
no warnings 'uninitialized'; # for 5.11.0
if(lc $_->attr('type') eq 'radio') {
my $name = name $_;
exists $pos{$name} ? push @{$ret[$pos{$name}]}, $_
:( push(@ret, [$_]),
$pos{$name} = $#ret );
next
}
push @ret, $tag eq 'select'
? $_->attr('multiple')
? $_->find('option')
: scalar $_->options
: $_
}
map ref $_ eq 'ARRAY' ? new HTML::DOM::NodeList::Radio $_ : $_,
@ret
}
sub click # 22/Sep/7: stolen from HTML::Form and modified (particularly
{ # the last line) so I don't have to mess with Hook::LexWrap
my $self = shift;
my $name;
$name = shift if (@_ % 2) == 1; # odd number of arguments
# try to find first submit button to activate
for ($self->inputs) {
next unless $_->type =~ /^(?:submit|image)\z/;
next if $name && $_->name ne $name;
next if $_->disabled;
$_->click($self, @_);return
}
Carp::croak("No clickable input with name $name") if $name;
$self->trigger_event('submit');
}
# These three were shamelessly stolen from HTML::Form:
sub value
{
package
HTML::Form;
my $self = shift;
my $key = shift;
my $input = $self->find_input($key);
Carp::croak("No such field '$key'") unless $input;
local $Carp::CarpLevel = 1;
$input->value(@_);
}
sub find_input
{
package
HTML::Form; # so caller tricks work
my($self, $name, $type, $no) = @_;
if (wantarray) {
my @res;
my $c;
for ($self->inputs) {
if (defined $name) {
next unless defined(my $n = $_->name);
next if $name ne $n;
}
next if $type && $type ne $_->type;
$c++;
next if $no && $no != $c;
push(@res, $_);
}
return @res;
}
else {
$no ||= 1;
for ($self->inputs) {
if (defined $name) {
next unless defined(my $n = $_->name);
next if $name ne $n;
}
next if $type && $type ne $_->type;
next if --$no;
return $_;
}
return undef;
}
}
sub param {
package
HTML::Form;
my $self = shift;
if (@_) {
my $name = shift;
my @inputs;
for ($self->inputs) {
my $n = $_->name;
next if !defined($n) || $n ne $name;
push(@inputs, $_);
}
if (@_) {
# set
die "No '$name' parameter exists" unless @inputs;
my @v = @_;
@v = @{$v[0]} if @v == 1 && ref($v[0]);
while (@v) {
my $v = shift @v;
my $err;
for my $i (0 .. @inputs-1) {
eval {
$inputs[$i]->value($v);
};
unless ($@) {
undef($err);
splice(@inputs, $i, 1);
last;
}
$err ||= $@;
}
die $err if $err;
}
# the rest of the input should be cleared
for (@inputs) {
$_->value(undef);
}
}
else {
# get
my @v;
for (@inputs) {
if (defined(my $v = $_->value)) {
push(@v, $v);
}
}
return wantarray ? @v : $v[0];
}
}
else {
# list parameter names
my @n;
my %seen;
for ($self->inputs) {
my $n = $_->name;
next if !defined($n) || $seen{$n}++;
push(@n, $n);
}
return @n;
}
}
my $ascii_encodings_re;
my $encodings_re;
sub _encoding_ok {
my ($enc,$xwfu) =@ _;
$enc =~ s/^(?:x-?)?mac-?/mac/i;
($enc) x (Encode'resolve_alias($enc)||return)
=~ ($xwfu ? $ascii_encodings_re : $encodings_re ||=qr/^${\
join'|',map quotemeta,
encodings Encode 'Byte'
}\z/);
}
sub _apply_charset {
my($charsets,$apply_to) = @_; # array refs
my ($charset,@ret);
for(@$charsets) {
#use DDS; Dump $_ if @$apply_to == 1;
eval {
@ret = ();
# Can’t use map here, because it could die. (In
# perl5.8.x, dying inside a map is a very
# bad idea.)
for my $applyee(@$apply_to) {
push @ret, ref $applyee
? $applyee
: Encode::encode(
$_,$applyee,9
); # 1=croak, 8=leave src alone
}
# Phew, we survived!
$charset = $_;
} && last;
}
unless($charset) {
# If none of the charsets applied, we just use the first
# one in the list (or fall back to utf8, since that’s the
# sensible thing to do these days), replacing unknown
# chars with ?
my $fallback;
$charset = $$charsets[0]||(++$fallback,'utf8');
@ret = map ref$_ ? $_ : Encode'encode($charset,$_),
@$apply_to;
$fallback and $charset = 'utf-8';
}
$charset,\@ret;
}
# ~~~ This does not take non-ASCII file names into account, but I can’t
# really do that yet, since perl itself doesn’t support those properly
# yet, either.
# This one was stolen from HTML::Form but then modified extensively.
sub make_request
{
my $self = shift;
my $method = $self->method;
my $uri = $self->action;
my $xwfu = $method eq 'get'
|| $self->enctype !~ /^multipart\/form-data\z/i;
my @form = $self->form;
# Get the charset and encode the form fields, if necessary. The HTML
# spec says that the a/x-w-f-u MIME type only accepts ASCII, but we’ll
# be more forgiving, for the sake of realism. But to be compliant with
# the spec in cases where it can apply (e.g., a UTF-16 page with just
# ASCII in its form data), we only accept ASCII-based encodings for
# this enctype.
my @charsets;
{ push @charsets, split ' ', $self->acceptCharset||next}
require Encode;
@charsets = map _encoding_ok($_, $xwfu),
@charsets;
unless(@charsets){{
# We only revert to the doc charset when accept-charset doesn’t
# have any usable encodings (even encodings which will cause char
# substitutions are considered usable; it’s non-ASCII with GET that
# we don’t want).
push @charsets, _encoding_ok(
($self->ownerDocument||next)->charset || next, $xwfu
)
}}
if ($method ne "post") {
require HTTP::Request;
$uri = URI->new($uri, "http");
$uri->query_form(@{_apply_charset \@charsets, \@form});
return HTTP::Request->new(GET => $uri);
}
else {
require HTTP::Request::Common;
if($xwfu) {
my($charset,$form) = _apply_charset \@charsets, \@form;
return HTTP::Request::Common::POST($uri, $form,
Content_Type =>
"application/x-www-form-urlencoded; charset=\"$charset\"");
}
else {
my @new_form;
while(@form) {
my($name,$val) = (shift @form, shift @form);
#my $origval = $val;
(my $charset, $val) = _apply_charset \@charsets, [$val];
#use DDS; Dump [$origval,$val, ];
push @new_form, Encode'encode('MIME-B',$name),
ref $$val[0] ? $$val[0] : [(undef)x2,
Content_Type => "text/plain; charset=\"$charset\"",
Content => @$val,
];
}
return HTTP::Request::Common::POST($uri, \@new_form,
Content_Type => 'multipart/form-data'
);
}
}
}
sub form
{
package
HTML::Form; # so caller tricks work
my $self = shift;
map { $_->form_name_value($self) } $self->inputs;
}
package HTML::DOM::NodeList::Radio; # solely for HTML::Form compatibility
# Usually ::Input is used, but ::Radio
# is for a set of radio buttons.
use Carp 'croak';
require HTML::DOM::NodeList;
our $VERSION = '0.035';
our @ISA = qw'HTML::DOM::NodeList';
sub type { 'radio' }
sub name {
my $ret = (my $self = shift)->item(0)->attr('name');
if (@_) {
$self->item($_)->attr(name=>@_) for 0..$self->length-1;
}
$ret
}
sub value { # ~~~ do case-folding and what-not, as in HTML::Form::ListInput
my $self = shift;
my $checked_elem;
for (0..$self->length-1) {
my $btn = $self->item($_);
$btn->checked and
$checked_elem = $btn, last;
}
if (@_) { for (0..$self->length-1) {
my $btn = $self->item($_);
$_[0] eq $btn->attr('value') and
$btn->disabled && croak(
"The value '$_[0]' has been disabled for field '${\
$self->name}'"
),
$btn->checked(1),
last;
}}
$checked_elem && $checked_elem->attr('value')
}
sub possible_values {
my $self = shift;
map $self->item($_)->attr('value'), 0..$self->length-1
}
sub disabled {
my $self = shift;
for(@$self) {
$_->disabled or return 0
}
return 1
}
sub form_name_value
# Pilfered from HTML::Form with slight changes.
{
package
HTML::Form::Input;
my $self = shift;
my $name = $self->name;
return unless defined $name && length $name;
return if $self->disabled;
my $value = $self->value;
return unless defined $value;
return ($name => $value);
}
package HTML::DOM::Collection::Elements;
use strict;
use warnings;
use Scalar::Util 'weaken';
our $VERSION = '0.035';
require HTML::DOM::Collection;
our @ISA = 'HTML::DOM::Collection';
# Internals: \[$nodelist, $tie]
# Field constants:
sub nodelist(){0}
sub tye(){1}
sub seen(){2} # whether this key has been seen
sub position(){3} # current (array) position used by NEXTKEY
sub ids(){4} # whether we are iterating through ids
{ no warnings 'misc';
undef &nodelist; undef &tye; undef &seen; undef &position;
}
sub namedItem {
my($self, $name) = @_;
my $list = $$self->[nodelist];
my $elem;
my @list;
for(0..$list->length - 1) {
no warnings 'uninitialized';
push @list, $elem if
($elem = $list->item($_))->id eq $name
or
$elem->attr('name') eq $name;
}
if(@list > 1) {
# ~~~ Perhaps this should cache the new nodelist
# and return the same one each item. (Incident-
# ally, Firefox returns the same one but Safari
# makes a new one each time.)
my $ret = HTML::DOM::NodeList::Magic->new(sub {
no warnings 'uninitialized';
grep $_->id eq $name ||
$_->attr('name') eq $name, @$list;
});
return $ret;
}
@list ? $list[0] :()
}
# ----------------- Docs ----------------- #
=head1 NAME
HTML::DOM::Element::Form - A Perl class for representing 'form' elements in an HTML DOM tree
=head1 SYNOPSIS
use HTML::DOM;
$doc = HTML::DOM->new;
$elem = $doc->createElement('form');
$elem->method('GET') # set attribute
$elem->method; # get attribute
$elem->enctype;
$elem->tagName;
# etc
=head1 DESCRIPTION
This class implements 'form' elements in an HTML::DOM tree. It
implements the HTMLFormElement DOM interface and inherits from
L (q.v.).
A form object can be used as a hash or an array, to access its input
fields, so S<<< C<< $form->[0] >> >>> and S<<< C<< $form->{name} >> >>>
are shorthand for
S<<< C<< $form->elements->[0] >> >>> and
S<< C<<< $form->elements->{name} >>> >>, respectively.
This class also tries to mimic L, but is not entirely
compatible
with its interface. See L, below.
=head1 DOM METHODS
In addition to those inherited from HTML::DOM::Element and
HTML::DOM::Node, this class implements the following DOM methods:
=over 4
=item elements
Returns a collection (L object) in scalar
context,
or a list in list context, of all the input
fields this form contains. This differs slightly from the C method
(part of the HTML::Form interface) in that it includes 'button' elements,
whereas C does not (though it does include 'input' elements with
'button' for the type).
=item length
Same as C<< $form->elements->length >>.
=item name
=item acceptCharset
=item action
=item enctype
=item method
=item target
Each of these returns the corresponding HTML attribute (C
corresponds to the 'accept-charset' attribute). If you pass an
argument, it will become the new value of the attribute, and the old value
will be returned.
=item submit
This triggers the form's 'submit' event, calling the default event handler
(see L). It is up to the default event handler to
take any further action. The form's C method may come in
handy.
This method is actually just short for $form->trigger_event('submit'). (See
L.)
=item reset
This triggers the form's 'reset' event.
=item trigger_event
This class overrides the superclasses' method to trigger the default event
handler for form submissions, when the submit event occurs, and reset the
form when a reset event occurs.
=back
=head1 WWW::Mechanize COMPATIBILITY
In order to work with L, this module mimics, and is
partly compatible with the
interface of, L.
HTML::Form's class methods do not apply. If you call
C<< HTML::DOM::Element::Form->parse >>, for instance, you'll just get an
error, because it doesn't exist.
The C and C methods do not exist either.
The C method behaves differently from HTML::Form's, in that it does
not call C, but triggers a 'click' event if there is a
button to click, or a 'submit' event otherwise.
The C, C, C, C, C, C,
C, C , C and C