module WWW
# Class Form does not work in the case there is some invalid (unbalanced) html
# involved, such as:
#
#
#
#
#
#
#
#
# GlobalForm takes two nodes, the node where the form tag is located
# (form_node), and another node, from which to start looking for form elements
# (elements_node) like buttons and the like. For class Form both fall together
# into one and the same node.
class GlobalForm
attr_reader :form_node, :elements_node
attr_accessor :method, :action, :name
attr_finder :fields, :buttons, :file_uploads, :radiobuttons, :checkboxes
attr_reader :enctype
def initialize(form_node, elements_node)
@form_node, @elements_node = form_node, elements_node
@method = (@form_node.attributes['method'] || 'GET').upcase
@action = @form_node.attributes['action']
@name = @form_node.attributes['name']
@enctype = @form_node.attributes['enctype'] || 'application/x-www-form-urlencoded'
@clicked_buttons = []
parse
end
# In the case of malformed HTML, fields of multiple forms might occure in this forms'
# field array. If the fields have the same name, posterior fields overwrite former fields.
# To avoid this, this method rejects all posterior duplicate fields.
def uniq_fields!
names_in = {}
fields.reject! {|f|
if names_in.include?(f.name)
true
else
names_in[f.name] = true
false
end
}
end
def build_query(buttons = [])
query = []
fields().each do |f|
next unless f.value
query << [f.name, f.value]
end
checkboxes().each do |f|
query << [f.name, f.value || "on"] if f.checked
end
radio_groups = {}
radiobuttons().each do |f|
radio_groups[f.name] ||= []
radio_groups[f.name] << f
end
# take one radio button from each group
radio_groups.each_value do |g|
checked = g.select {|f| f.checked}
if checked.size == 1
f = checked.first
query << [f.name, f.value || ""]
elsif checked.size > 1
raise "multiple radiobuttons are checked in the same group!"
end
end
@clicked_buttons.each { |b|
b.add_to_query(query)
}
query
end
def add_button_to_query(button)
@clicked_buttons << button
end
def request_data
query_params = build_query()
query = nil
case @enctype.downcase
when 'multipart/form-data'
boundary = rand_string(20)
@enctype << ", boundary=#{boundary}"
params = []
query_params.each { |k,v| params << param_to_multipart(k, v) }
@file_uploads.each { |f| params << file_to_multipart(f) }
query = params.collect { |p| "--#{boundary}\r\n#{p}" }.join('') +
"--#{boundary}--\r\n"
else
query = WWW::Mechanize.build_query_string(query_params)
end
query
end
def parse
@fields = WWW::Mechanize::List.new
@buttons = WWW::Mechanize::List.new
@file_uploads = WWW::Mechanize::List.new
@radiobuttons = WWW::Mechanize::List.new
@checkboxes = WWW::Mechanize::List.new
@elements_node.each_recursive {|node|
case node.name.downcase
when 'input'
case (node.attributes['type'] || 'text').downcase
when 'text', 'password', 'hidden', 'int'
@fields << Field.new(node.attributes['name'], node.attributes['value'] || '')
when 'radio'
@radiobuttons << RadioButton.new(node.attributes['name'], node.attributes['value'], node.attributes.has_key?('checked'))
when 'checkbox'
@checkboxes << CheckBox.new(node.attributes['name'], node.attributes['value'], node.attributes.has_key?('checked'))
when 'file'
@file_uploads << FileUpload.new(node.attributes['name'], node.attributes['value'])
when 'submit'
@buttons << Button.new(node.attributes['name'], node.attributes['value'])
when 'image'
@buttons << ImageButton.new(node.attributes['name'], node.attributes['value'])
end
when 'textarea'
@fields << Field.new(node.attributes['name'], node.all_text)
when 'select'
@fields << SelectList.new(node.attributes['name'], node)
end
}
end
def inspect
string = "Form: ['#{@name}' -> #{@action}]\n"
string << "[radiobuttons]\n"
@radiobuttons.each { |f| string << f.inspect }
string << "[checkboxes]\n"
@checkboxes.each { |f| string << f.inspect }
string << "[fields]\n"
@fields.each { |f| string << f.inspect }
string << "[buttons]\n"
@buttons.each { |f| string << f.inspect }
string
end
private
def rand_string(len = 10)
chars = ("a".."z").to_a + ("A".."Z").to_a
string = ""
1.upto(len) { |i| string << chars[rand(chars.size-1)] }
string
end
def mime_value_quote(str)
str.gsub(/(["\r\\])/){|s| '\\' + s}
end
def param_to_multipart(name, value)
return "Content-Disposition: form-data; name=\"" +
"#{mime_value_quote(name)}\"\r\n" +
"\r\n#{value}\r\n"
end
def file_to_multipart(file)
body = "Content-Disposition: form-data; name=\"" +
"#{mime_value_quote(file.name)}\"; " +
"filename=\"#{mime_value_quote(file.file_name)}\"\r\n" +
"Content-Transfer-Encoding: binary\r\n"
if file.mime_type != nil
body << "Content-Type: #{file.mime_type}\r\n"
end
body << "\r\n#{file.file_data}\r\n"
body
end
end
class Form < GlobalForm
attr_reader :node
def initialize(node)
@node = node
super(@node, @node)
end
end
end