require'mechanize/element_matcher'require'mechanize/form/field'require'mechanize/form/button'require'mechanize/form/file_upload'require'mechanize/form/image_button'require'mechanize/form/multi_select_list'require'mechanize/form/option'require'mechanize/form/radio_button'require'mechanize/form/check_box'require'mechanize/form/select_list'classMechanize# =Synopsis# This class encapsulates a form parsed out of an HTML page. Each type# of input fields available in a form can be accessed through this object.# See GlobalForm for more methods.## ==Example# Find a form and print out its fields# form = page.forms.first # => Mechanize::Form# form.fields.each { |f| puts f.name }# Set the input field 'name' to "Aaron"# form['name'] = 'Aaron'# puts form['name']classFormextendMechanize::ElementMatcherattr_accessor:method,:action,:nameattr_reader:fields,:buttons,:file_uploads,:radiobuttons,:checkboxesattr_accessor:enctypealias:elements:fieldsattr_reader:form_nodeattr_reader:pagedefinitialize(node,mech=nil,page=nil)@enctype=node['enctype']||'application/x-www-form-urlencoded'@form_node=node@action=Util.html_unescape(node['action'])@method=(node['method']||'GET').upcase@name=node['name']@clicked_buttons=[]@page=page@mech=mechparseend# Returns whether or not the form contains a field with +field_name+defhas_field?(field_name)fields.find{|f|f.name==field_name}endalias:has_key?:has_field?defhas_value?(value)fields.find{|f|f.value==value}enddefkeys;fields.map{|f|f.name};enddefvalues;fields.map{|f|f.value};enddefsubmits;@submits||=buttons.select{|f|f.class==Submit};enddefresets;@resets||=buttons.select{|f|f.class==Reset};enddeftexts;@texts||=fields.select{|f|f.class==Text};enddefhiddens;@hiddens||=fields.select{|f|f.class==Hidden};enddeftextareas;@textareas||=fields.select{|f|f.class==Textarea};enddefsubmit_button?(button_name)submits.find{|f|f.name==button_name};enddefreset_button?(button_name)resets.find{|f|f.name==button_name};enddeftext_field?(field_name)texts.find{|f|f.name==field_name};enddefhidden_field?(field_name)hiddens.find{|f|f.name==field_name};enddeftextarea_field?(field_name)textareas.find{|f|f.name==field_name};end# This method is a shortcut to get form's DOM id.# Common usage:# page.form_with(:dom_id => "foorm")# Note that you can also use +:id+ to get to this method:# page.form_with(:id => "foorm")defdom_idform_node['id']end# Add a field with +field_name+ and +value+defadd_field!(field_name,value=nil)fields<<Field.new({'name'=>field_name},value)end# This method sets multiple fields on the form. It takes a list of field# name, value pairs. If there is more than one field found with the# same name, this method will set the first one found. If you want to# set the value of a duplicate field, use a value which is a Hash with# the key as the index in to the form. The index# is zero based. For example, to set the second field named 'foo', you# could do the following:# form.set_fields( :foo => { 1 => 'bar' } )defset_fields(fields={})fields.eachdo|k,v|casevwhenHashv.eachdo|index,value|self.fields_with(:name=>k.to_s).[](index).value=valueendelsevalue=nilindex=0[v].flatten.eachdo|val|index=val.to_iifvaluevalue=valunlessvalueendself.fields_with(:name=>k.to_s).[](index).value=valueendendend# Fetch the value of the first input field with the name passed in# ==Example# Fetch the value set in the input field 'name'# puts form['name']def[](field_name)f=field(field_name)f&&f.valueend# Set the value of the first input field with the name passed in# ==Example# Set the value in the input field 'name' to "Aaron"# form['name'] = 'Aaron'def[]=(field_name,value)f=field(field_name)ifff.value=valueelseadd_field!(field_name,value)endend# Treat form fields like accessors.defmethod_missing(meth,*args)method=meth.to_s.gsub(/=$/,'')iffield(method)returnfield(method).valueifargs.empty?returnfield(method).value=args[0]endsuperend# Submit this form with the button passed indefsubmitbutton=nil,headers={}@mech.submit(self,button,headers)end# Submit form using +button+. Defaults# to the first button.defclick_button(button=buttons.first)submit(button)end# This method is sub-method of build_query.# It converts charset of query value of fields into expected one.defproc_query(field)returnunlessfield.query_valuefield.query_value.map{|(name,val)|[from_native_charset(name),from_native_charset(val.to_s)]}endprivate:proc_querydeffrom_native_charsetstrUtil.from_native_charset(str,page&&page.encoding)endprivate:from_native_charset# This method builds an array of arrays that represent the query# parameters to be used with this form. The return value can then# be used to create a query string for this form.defbuild_query(buttons=[])query=[](fields+checkboxes).sort.eachdo|f|casefwhenForm::CheckBoxiff.checkedqval=proc_query(f)query.push(*qval)endwhenForm::Fieldqval=proc_query(f)query.push(*qval)endendradio_groups={}radiobuttons.eachdo|f|fname=from_native_charset(f.name)radio_groups[fname]||=[]radio_groups[fname]<<fend# take one radio button from each groupradio_groups.each_valuedo|g|checked=g.select{|f|f.checked}ifchecked.size==1f=checked.firstqval=proc_query(f)query.push(*qval)elsifchecked.size>1raiseMechanize::Error,"multiple radiobuttons are checked in the same group!"endend@clicked_buttons.each{|b|qval=proc_query(b)query.push(*qval)}queryend# This method adds a button to the query. If the form needs to be# submitted with multiple buttons, pass each button to this method.defadd_button_to_query(button)@clicked_buttons<<buttonend# This method calculates the request data to be sent back to the server# for this form, depending on if this is a regular post, get, or a# multi-part post,defrequest_dataquery_params=build_query()case@enctype.downcasewhen/^multipart\/form-data/boundary=rand_string(20)@enctype="multipart/form-data; boundary=#{boundary}"params=query_params.mapdo|k,v|param_to_multipart(k,v)ifkend.compactparams.concat@file_uploads.map{|f|file_to_multipart(f)}params.mapdo|part|part.force_encoding('ASCII-8BIT')ifpart.respond_to?:force_encoding"--#{boundary}\r\n#{part}"end.join('')+"--#{boundary}--\r\n"elseMechanize::Util.build_query_string(query_params)endend# Removes all fields with name +field_name+.defdelete_field!(field_name)@fields.delete_if{|f|f.name==field_name}end### :method: field_with(criteria)## Find one field that matches +criteria+# Example:# form.field_with(:id => "exact_field_id").value = 'hello'### :method: fields_with(criteria)## Find all fields that match +criteria+# Example:# form.fields_with(:value => /foo/).each do |field|# field.value = 'hello!'# endelements_with:field### :method: button_with(criteria)## Find one button that matches +criteria+# Example:# form.button_with(:value => /submit/).value = 'hello'### :method: buttons_with(criteria)## Find all buttons that match +criteria+# Example:# form.buttons_with(:value => /submit/).each do |button|# button.value = 'hello!'# endelements_with:button### :method: file_upload_with(criteria)## Find one file upload field that matches +criteria+# Example:# form.file_upload_with(:file_name => /picture/).value = 'foo'### :method: file_uploads_with(criteria)## Find all file upload fields that match +criteria+# Example:# form.file_uploads_with(:file_name => /picutre/).each do |field|# field.value = 'foo!'# endelements_with:file_upload### :method: radiobutton_with(criteria)## Find one radio button that matches +criteria+# Example:# form.radiobutton_with(:name => /woo/).check### :method: radiobuttons_with(criteria)## Find all radio buttons that match +criteria+# Example:# form.radiobuttons_with(:name => /woo/).each do |field|# field.check# endelements_with:radiobutton### :method: checkbox_with(criteria)## Find one checkbox that matches +criteria+# Example:# form.checkbox_with(:name => /woo/).check### :method: checkboxes_with(criteria)## Find all checkboxes that match +criteria+# Example:# form.checkboxes_with(:name => /woo/).each do |field|# field.check# endelements_with:checkbox,:checkboxesprivatedefparse@fields=[]@buttons=[]@file_uploads=[]@radiobuttons=[]@checkboxes=[]# Find all input tagsform_node.search('input').eachdo|node|type=(node['type']||'text').downcasename=node['name']nextifname.nil?&&!%w[submit button image].include?(type)casetypewhen'radio'@radiobuttons<<RadioButton.new(node,self)when'checkbox'@checkboxes<<CheckBox.new(node,self)when'file'@file_uploads<<FileUpload.new(node,nil)when'submit'@buttons<<Submit.new(node)when'button'@buttons<<Button.new(node)when'reset'@buttons<<Reset.new(node)when'image'@buttons<<ImageButton.new(node)when'hidden'@fields<<Hidden.new(node,node['value']||'')when'text'@fields<<Text.new(node,node['value']||'')when'textarea'@fields<<Textarea.new(node,node['value']||'')else@fields<<Field.new(node,node['value']||'')endend# Find all textarea tagsform_node.search('textarea').eachdo|node|nextunlessnode['name']@fields<<Field.new(node,node.inner_text)end# Find all select tagsform_node.search('select').eachdo|node|nextunlessnode['name']ifnode.has_attribute?'multiple'@fields<<MultiSelectList.new(node)else@fields<<SelectList.new(node)endend# Find all submit button tags# FIXME: what can I do with the reset buttons?form_node.search('button').eachdo|node|type=(node['type']||'submit').downcasenextiftype=='reset'@buttons<<Button.new(node)endenddefrand_string(len=10)chars=("a".."z").to_a+("A".."Z").to_astring=""1.upto(len){|i|string<<chars[rand(chars.size-1)]}stringenddefmime_value_quote(str)str.gsub(/(["\r\\])/){|s|'\\'+s}enddefparam_to_multipart(name,value)return"Content-Disposition: form-data; name=\""+"#{mime_value_quote(name)}\"\r\n"+"\r\n#{value}\r\n"enddeffile_to_multipart(file)file_name=file.file_name?::File.basename(file.file_name):''body="Content-Disposition: form-data; name=\""+"#{mime_value_quote(file.name)}\"; "+"filename=\"#{mime_value_quote(file_name)}\"\r\n"+"Content-Transfer-Encoding: binary\r\n"iffile.file_data.nil?andfile.file_namefile.file_data=open(file.file_name,"rb"){|f|f.read}file.mime_type=WEBrick::HTTPUtils.mime_type(file.file_name,WEBrick::HTTPUtils::DefaultMimeTypes)endiffile.mime_typebody<<"Content-Type: #{file.mime_type}\r\n"endbody<<iffile.file_data.respond_to?:read"\r\n#{file.file_data.read}\r\n"else"\r\n#{file.file_data}\r\n"endbodyendendend