kitamomonga-mechanize 0.9.3.20090724215219

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. data/CHANGELOG.rdoc +504 -0
  2. data/EXAMPLES.rdoc +171 -0
  3. data/FAQ.rdoc +11 -0
  4. data/GUIDE.rdoc +122 -0
  5. data/LICENSE.rdoc +340 -0
  6. data/Manifest.txt +176 -0
  7. data/README.rdoc +60 -0
  8. data/Rakefile +33 -0
  9. data/examples/flickr_upload.rb +23 -0
  10. data/examples/mech-dump.rb +7 -0
  11. data/examples/proxy_req.rb +9 -0
  12. data/examples/rubyforge.rb +21 -0
  13. data/examples/spider.rb +11 -0
  14. data/lib/mechanize.rb +666 -0
  15. data/lib/mechanize/chain.rb +34 -0
  16. data/lib/mechanize/chain/auth_headers.rb +78 -0
  17. data/lib/mechanize/chain/body_decoding_handler.rb +46 -0
  18. data/lib/mechanize/chain/connection_resolver.rb +76 -0
  19. data/lib/mechanize/chain/custom_headers.rb +21 -0
  20. data/lib/mechanize/chain/handler.rb +9 -0
  21. data/lib/mechanize/chain/header_resolver.rb +51 -0
  22. data/lib/mechanize/chain/parameter_resolver.rb +22 -0
  23. data/lib/mechanize/chain/post_connect_hook.rb +0 -0
  24. data/lib/mechanize/chain/post_page_hook.rb +18 -0
  25. data/lib/mechanize/chain/pre_connect_hook.rb +20 -0
  26. data/lib/mechanize/chain/request_resolver.rb +30 -0
  27. data/lib/mechanize/chain/response_body_parser.rb +38 -0
  28. data/lib/mechanize/chain/response_header_handler.rb +48 -0
  29. data/lib/mechanize/chain/response_reader.rb +39 -0
  30. data/lib/mechanize/chain/ssl_resolver.rb +40 -0
  31. data/lib/mechanize/chain/uri_resolver.rb +75 -0
  32. data/lib/mechanize/content_type_error.rb +14 -0
  33. data/lib/mechanize/cookie.rb +70 -0
  34. data/lib/mechanize/cookie_jar.rb +188 -0
  35. data/lib/mechanize/file.rb +71 -0
  36. data/lib/mechanize/file_response.rb +60 -0
  37. data/lib/mechanize/file_saver.rb +37 -0
  38. data/lib/mechanize/form.rb +364 -0
  39. data/lib/mechanize/form/button.rb +7 -0
  40. data/lib/mechanize/form/check_box.rb +11 -0
  41. data/lib/mechanize/form/field.rb +26 -0
  42. data/lib/mechanize/form/file_upload.rb +22 -0
  43. data/lib/mechanize/form/image_button.rb +21 -0
  44. data/lib/mechanize/form/multi_select_list.rb +67 -0
  45. data/lib/mechanize/form/option.rb +49 -0
  46. data/lib/mechanize/form/radio_button.rb +36 -0
  47. data/lib/mechanize/form/select_list.rb +43 -0
  48. data/lib/mechanize/headers.rb +11 -0
  49. data/lib/mechanize/history.rb +65 -0
  50. data/lib/mechanize/inspect.rb +88 -0
  51. data/lib/mechanize/monkey_patch.rb +35 -0
  52. data/lib/mechanize/page.rb +279 -0
  53. data/lib/mechanize/page/base.rb +8 -0
  54. data/lib/mechanize/page/encoding.rb +61 -0
  55. data/lib/mechanize/page/frame.rb +20 -0
  56. data/lib/mechanize/page/link.rb +53 -0
  57. data/lib/mechanize/page/meta.rb +50 -0
  58. data/lib/mechanize/pluggable_parsers.rb +101 -0
  59. data/lib/mechanize/redirect_limit_reached_error.rb +16 -0
  60. data/lib/mechanize/redirect_not_get_or_head_error.rb +18 -0
  61. data/lib/mechanize/response_code_error.rb +22 -0
  62. data/lib/mechanize/unsupported_scheme_error.rb +8 -0
  63. data/lib/mechanize/util.rb +73 -0
  64. data/test/chain/test_argument_validator.rb +14 -0
  65. data/test/chain/test_auth_headers.rb +25 -0
  66. data/test/chain/test_custom_headers.rb +18 -0
  67. data/test/chain/test_header_resolver.rb +28 -0
  68. data/test/chain/test_parameter_resolver.rb +35 -0
  69. data/test/chain/test_request_resolver.rb +29 -0
  70. data/test/chain/test_response_reader.rb +24 -0
  71. data/test/data/htpasswd +1 -0
  72. data/test/data/server.crt +16 -0
  73. data/test/data/server.csr +12 -0
  74. data/test/data/server.key +15 -0
  75. data/test/data/server.pem +15 -0
  76. data/test/helper.rb +129 -0
  77. data/test/htdocs/alt_text.html +10 -0
  78. data/test/htdocs/bad_form_test.html +9 -0
  79. data/test/htdocs/button.jpg +0 -0
  80. data/test/htdocs/empty_form.html +6 -0
  81. data/test/htdocs/file_upload.html +26 -0
  82. data/test/htdocs/find_link.html +41 -0
  83. data/test/htdocs/form_multi_select.html +16 -0
  84. data/test/htdocs/form_multival.html +37 -0
  85. data/test/htdocs/form_no_action.html +18 -0
  86. data/test/htdocs/form_no_input_name.html +16 -0
  87. data/test/htdocs/form_select.html +16 -0
  88. data/test/htdocs/form_select_all.html +16 -0
  89. data/test/htdocs/form_select_none.html +17 -0
  90. data/test/htdocs/form_select_noopts.html +10 -0
  91. data/test/htdocs/form_set_fields.html +14 -0
  92. data/test/htdocs/form_test.html +188 -0
  93. data/test/htdocs/frame_test.html +30 -0
  94. data/test/htdocs/google.html +13 -0
  95. data/test/htdocs/iframe_test.html +16 -0
  96. data/test/htdocs/index.html +6 -0
  97. data/test/htdocs/link with space.html +5 -0
  98. data/test/htdocs/meta_cookie.html +11 -0
  99. data/test/htdocs/no_title_test.html +6 -0
  100. data/test/htdocs/relative/tc_relative_links.html +21 -0
  101. data/test/htdocs/tc_bad_charset.html +9 -0
  102. data/test/htdocs/tc_bad_links.html +5 -0
  103. data/test/htdocs/tc_base_link.html +8 -0
  104. data/test/htdocs/tc_blank_form.html +11 -0
  105. data/test/htdocs/tc_charset.html +6 -0
  106. data/test/htdocs/tc_checkboxes.html +19 -0
  107. data/test/htdocs/tc_encoded_links.html +5 -0
  108. data/test/htdocs/tc_follow_meta.html +8 -0
  109. data/test/htdocs/tc_form_action.html +48 -0
  110. data/test/htdocs/tc_links.html +19 -0
  111. data/test/htdocs/tc_no_attributes.html +16 -0
  112. data/test/htdocs/tc_pretty_print.html +17 -0
  113. data/test/htdocs/tc_radiobuttons.html +17 -0
  114. data/test/htdocs/tc_referer.html +10 -0
  115. data/test/htdocs/tc_relative_links.html +19 -0
  116. data/test/htdocs/tc_textarea.html +23 -0
  117. data/test/htdocs/test_bad_encoding.html +52 -0
  118. data/test/htdocs/unusual______.html +5 -0
  119. data/test/servlets.rb +365 -0
  120. data/test/ssl_server.rb +48 -0
  121. data/test/test_authenticate.rb +71 -0
  122. data/test/test_bad_links.rb +25 -0
  123. data/test/test_blank_form.rb +16 -0
  124. data/test/test_checkboxes.rb +61 -0
  125. data/test/test_content_type.rb +13 -0
  126. data/test/test_cookie_class.rb +338 -0
  127. data/test/test_cookie_jar.rb +362 -0
  128. data/test/test_cookies.rb +123 -0
  129. data/test/test_encoded_links.rb +20 -0
  130. data/test/test_errors.rb +49 -0
  131. data/test/test_follow_meta.rb +108 -0
  132. data/test/test_form_action.rb +52 -0
  133. data/test/test_form_as_hash.rb +61 -0
  134. data/test/test_form_button.rb +38 -0
  135. data/test/test_form_no_inputname.rb +15 -0
  136. data/test/test_forms.rb +577 -0
  137. data/test/test_frames.rb +25 -0
  138. data/test/test_get_headers.rb +73 -0
  139. data/test/test_gzipping.rb +22 -0
  140. data/test/test_hash_api.rb +45 -0
  141. data/test/test_history.rb +142 -0
  142. data/test/test_history_added.rb +16 -0
  143. data/test/test_html_unscape_forms.rb +39 -0
  144. data/test/test_if_modified_since.rb +20 -0
  145. data/test/test_keep_alive.rb +31 -0
  146. data/test/test_links.rb +127 -0
  147. data/test/test_mech.rb +289 -0
  148. data/test/test_mechanize_file.rb +72 -0
  149. data/test/test_meta.rb +65 -0
  150. data/test/test_multi_select.rb +106 -0
  151. data/test/test_no_attributes.rb +13 -0
  152. data/test/test_option.rb +18 -0
  153. data/test/test_page.rb +127 -0
  154. data/test/test_page_encoding.rb +298 -0
  155. data/test/test_pluggable_parser.rb +145 -0
  156. data/test/test_post_form.rb +34 -0
  157. data/test/test_pretty_print.rb +22 -0
  158. data/test/test_radiobutton.rb +75 -0
  159. data/test/test_redirect_limit_reached.rb +39 -0
  160. data/test/test_redirect_verb_handling.rb +43 -0
  161. data/test/test_referer.rb +39 -0
  162. data/test/test_relative_links.rb +40 -0
  163. data/test/test_request.rb +13 -0
  164. data/test/test_response_code.rb +52 -0
  165. data/test/test_save_file.rb +103 -0
  166. data/test/test_scheme.rb +63 -0
  167. data/test/test_select.rb +106 -0
  168. data/test/test_select_all.rb +15 -0
  169. data/test/test_select_none.rb +15 -0
  170. data/test/test_select_noopts.rb +16 -0
  171. data/test/test_set_fields.rb +44 -0
  172. data/test/test_ssl_server.rb +20 -0
  173. data/test/test_subclass.rb +14 -0
  174. data/test/test_textarea.rb +45 -0
  175. data/test/test_upload.rb +109 -0
  176. data/test/test_verbs.rb +25 -0
  177. metadata +320 -0
@@ -0,0 +1,71 @@
1
+ class Mechanize
2
+ # = Synopsis
3
+ # This is the default (and base) class for the Pluggable Parsers. If
4
+ # Mechanize cannot find an appropriate class to use for the content type,
5
+ # this class will be used. For example, if you download a JPG, Mechanize
6
+ # will not know how to parse it, so this class will be instantiated.
7
+ #
8
+ # This is a good class to use as the base class for building your own
9
+ # pluggable parsers.
10
+ #
11
+ # == Example
12
+ # require 'rubygems'
13
+ # require 'mechanize'
14
+ #
15
+ # agent = Mechanize.new
16
+ # agent.get('http://example.com/foo.jpg').class #=> Mechanize::File
17
+ #
18
+ class File
19
+ attr_accessor :uri, :response, :body, :code, :filename
20
+ alias :header :response
21
+
22
+ alias :content :body
23
+
24
+ def initialize(uri=nil, response=nil, body=nil, code=nil, mech=nil)
25
+ @uri, @body, @code, @mech = uri, body, code, mech
26
+ @response = Headers.new
27
+
28
+ # Copy the headers in to a hash to prevent memory leaks
29
+ if response
30
+ response.each { |k,v|
31
+ @response[k] = v
32
+ }
33
+ end
34
+
35
+ @filename = 'index.html'
36
+
37
+ # Set the filename
38
+ if @mech && @mech.use_content_disposition && (disposition = @response['content-disposition'])
39
+ disposition.split(/;\s*/).each do |pair|
40
+ k,v = pair.split(/=/, 2)
41
+ @filename = v.sub(/\A"/){''}.sub(/"\Z/){''} if k && k.downcase == 'filename'
42
+ end
43
+ else
44
+ if @uri
45
+ @filename = @uri.path.split(/\//).last || 'index.html'
46
+ @filename << ".html" unless @filename =~ /\./
47
+ end
48
+ end
49
+
50
+ yield self if block_given?
51
+ end
52
+
53
+ # Use this method to save the content of this object to filename
54
+ def save_as(filename = nil)
55
+ if filename.nil?
56
+ filename = @filename
57
+ number = 1
58
+ while(::File.exists?(filename))
59
+ filename = "#{@filename}.#{number}"
60
+ number += 1
61
+ end
62
+ end
63
+
64
+ ::File::open(filename, "wb") { |f|
65
+ f.write body
66
+ }
67
+ end
68
+
69
+ alias :save :save_as
70
+ end
71
+ end
@@ -0,0 +1,60 @@
1
+ class Mechanize
2
+ ###
3
+ # Fake response for dealing with file:/// requests
4
+ class FileResponse
5
+ def initialize(file_path)
6
+ @file_path = file_path
7
+ end
8
+
9
+ def read_body
10
+ if ::File.exists?(@file_path)
11
+ if directory?
12
+ yield dir_body
13
+ else
14
+ yield ::File.read(@file_path)
15
+ end
16
+ else
17
+ yield ''
18
+ end
19
+ end
20
+
21
+ def code
22
+ ::File.exists?(@file_path) ? '200' : '400'
23
+ end
24
+
25
+ def content_length
26
+ return dir_body.length if directory?
27
+ ::File.exists?(@file_path) ? ::File.stat(@file_path).size : 0
28
+ end
29
+
30
+ def each_header; end
31
+
32
+ def [](key)
33
+ return nil unless key.downcase == 'content-type'
34
+ return 'text/html' if directory?
35
+ return 'text/html' if ['.html', '.xhtml'].any? { |extn|
36
+ @file_path =~ /#{extn}$/
37
+ }
38
+ nil
39
+ end
40
+
41
+ def each
42
+ end
43
+
44
+ def get_fields(key)
45
+ []
46
+ end
47
+
48
+ private
49
+ def dir_body
50
+ '<html><body>' +
51
+ Dir[::File.join(@file_path, '*')].map { |f|
52
+ "<a href=\"file://#{f}\">#{::File.basename(f)}</a>"
53
+ }.join("\n") + '</body></html>'
54
+ end
55
+
56
+ def directory?
57
+ ::File.directory?(@file_path)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,37 @@
1
+ class Mechanize
2
+ # = Synopsis
3
+ # This is a pluggable parser that automatically saves every file
4
+ # it encounters. It saves the files as a tree, reflecting the
5
+ # host and file path.
6
+ #
7
+ # == Example to save all PDF's
8
+ # require 'rubygems'
9
+ # require 'mechanize'
10
+ #
11
+ # agent = Mechanize.new
12
+ # agent.pluggable_parser.pdf = Mechanize::FileSaver
13
+ # agent.get('http://example.com/foo.pdf')
14
+ #
15
+ class FileSaver < File
16
+ attr_reader :filename
17
+
18
+ def initialize(uri=nil, response=nil, body=nil, code=nil)
19
+ super(uri, response, body, code)
20
+ path = uri.path.empty? ? 'index.html' : uri.path.gsub(/^[\/]*/, '')
21
+ path += 'index.html' if path =~ /\/$/
22
+
23
+ split_path = path.split(/\//)
24
+ filename = split_path.length > 0 ? split_path.pop : 'index.html'
25
+ joined_path = split_path.join(::File::SEPARATOR)
26
+ path = if joined_path.empty?
27
+ uri.host
28
+ else
29
+ "#{uri.host}#{::File::SEPARATOR}#{joined_path}"
30
+ end
31
+
32
+ @filename = "#{path}#{::File::SEPARATOR}#{filename}"
33
+ FileUtils.mkdir_p(path)
34
+ save_as(@filename)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,364 @@
1
+ require 'mechanize/form/field'
2
+ require 'mechanize/form/file_upload'
3
+ require 'mechanize/form/button'
4
+ require 'mechanize/form/image_button'
5
+ require 'mechanize/form/radio_button'
6
+ require 'mechanize/form/check_box'
7
+ require 'mechanize/form/multi_select_list'
8
+ require 'mechanize/form/select_list'
9
+ require 'mechanize/form/option'
10
+
11
+ class Mechanize
12
+ # =Synopsis
13
+ # This class encapsulates a form parsed out of an HTML page. Each type
14
+ # of input fields available in a form can be accessed through this object.
15
+ #
16
+ # ==Example
17
+ # Find a form and print out its fields
18
+ # form = page.forms.first # => Mechanize::Form
19
+ # form.fields.each { |f| puts f.name }
20
+ # Set the input field 'name' to "Aaron"
21
+ # form['name'] = 'Aaron'
22
+ # puts form['name']
23
+ class Form
24
+ attr_accessor :method, :action, :name
25
+
26
+ attr_reader :fields, :buttons, :file_uploads, :radiobuttons, :checkboxes
27
+ attr_accessor :enctype
28
+
29
+ alias :elements :fields
30
+
31
+ attr_reader :form_node
32
+ alias :node :form_node
33
+
34
+ attr_reader :page
35
+
36
+ def initialize(node, mech=nil, page=nil)
37
+ @enctype = node['enctype'] || 'application/x-www-form-urlencoded'
38
+ @form_node = node
39
+ @action = Util.html_unescape(node['action'])
40
+ @method = (node['method'] || 'GET').upcase
41
+ @name = node['name']
42
+ @clicked_buttons = []
43
+ @page = page
44
+ @mech = mech
45
+
46
+ parse
47
+ end
48
+
49
+ # Returns whether or not the form contains a field with +field_name+
50
+ def has_field?(field_name)
51
+ ! field_with(:name => field_name).nil?
52
+ end
53
+
54
+ alias :has_key? :has_field?
55
+
56
+ def has_value?(value)
57
+ ! field_with(:value => value).nil?
58
+ end
59
+
60
+ def keys; fields.map { |f| f.name }; end
61
+
62
+ def values; fields.map { |f| f.value }; end
63
+
64
+ # Returns class attribute (<form class="***"> of ***). If no class, returns nil.
65
+ def attribute_class; form_node['class']; end
66
+ # Returns id attribute (<form id="***"> of ***). If no id, returns nil.
67
+ def attribute_id; form_node['id']; end
68
+
69
+ # Add a field with +field_name+ and +value+
70
+ def add_field!(field_name, value = nil)
71
+ fields << Field.new(field_name, value)
72
+ end
73
+
74
+ # This method sets multiple fields on the form. It takes a list of field
75
+ # name, value pairs. If there is more than one field found with the
76
+ # same name, this method will set the first one found. If you want to
77
+ # set the value of a duplicate field, use a value which is a Hash with
78
+ # the key as the index in to the form. The index
79
+ # is zero based. For example, to set the second field named 'foo', you
80
+ # could do the following:
81
+ # form.set_fields( :foo => { 1 => 'bar' } )
82
+ def set_fields(fields = {})
83
+ fields.each do |k,v|
84
+ case v
85
+ when Hash
86
+ v.each do |index, value|
87
+ self.fields_with(:name => k.to_s).[](index).value = value
88
+ end
89
+ else
90
+ value = nil
91
+ index = 0
92
+ [v].flatten.each do |val|
93
+ index = val.to_i unless value.nil?
94
+ value = val if value.nil?
95
+ end
96
+ self.fields_with(:name => k.to_s).[](index).value = value
97
+ end
98
+ end
99
+ end
100
+
101
+ # Fetch the value of the first input field with the name passed in
102
+ # ==Example
103
+ # Fetch the value set in the input field 'name'
104
+ # puts form['name']
105
+ def [](field_name)
106
+ f = field_with(:name => field_name)
107
+ f && f.value
108
+ end
109
+
110
+ # Set the value of the first input field with the name passed in
111
+ # ==Example
112
+ # Set the value in the input field 'name' to "Aaron"
113
+ # form['name'] = 'Aaron'
114
+ def []=(field_name, value)
115
+ f = field_with(:name => field_name)
116
+ if f.nil?
117
+ add_field!(field_name, value)
118
+ else
119
+ f.value = value
120
+ end
121
+ end
122
+
123
+ # Treat form fields like accessors.
124
+ def method_missing(id,*args)
125
+ method = id.to_s.gsub(/=$/, '')
126
+ if field_with(:name => method)
127
+ return field_with(:name => method).value if args.empty?
128
+ return field_with(:name => method).value = args[0]
129
+ end
130
+ super
131
+ end
132
+
133
+ # Submit this form with the button passed in
134
+ def submit button=nil, headers = {}
135
+ @mech.submit(self, button, headers)
136
+ end
137
+
138
+ # Submit form using +button+. Defaults
139
+ # to the first button.
140
+ def click_button(button = buttons.first)
141
+ submit(button)
142
+ end
143
+
144
+ # This method is sub-method of build_query.
145
+ # It converts charset of query value of fields into expected one.
146
+ def proc_query(field)
147
+ return unless field.query_value
148
+ field.query_value.map{|(name, val)|
149
+ [from_native_charset(name), from_native_charset(val.to_s)]
150
+ }
151
+ end
152
+ private :proc_query
153
+
154
+ def from_native_charset(str, enc=nil)
155
+ if page
156
+ enc ||= page.encoding
157
+ Util.from_native_charset(str,enc) rescue str
158
+ else
159
+ str
160
+ end
161
+ end
162
+ private :from_native_charset
163
+
164
+ # This method builds an array of arrays that represent the query
165
+ # parameters to be used with this form. The return value can then
166
+ # be used to create a query string for this form.
167
+ def build_query(buttons = [])
168
+ query = []
169
+
170
+ fields().each do |f|
171
+ qval = proc_query(f)
172
+ query.push(*qval)
173
+ end
174
+
175
+ checkboxes().each do |f|
176
+ if f.checked
177
+ qval = proc_query(f)
178
+ query.push(*qval)
179
+ end
180
+ end
181
+
182
+ radio_groups = {}
183
+ radiobuttons().each do |f|
184
+ fname = from_native_charset(f.name)
185
+ radio_groups[fname] ||= []
186
+ radio_groups[fname] << f
187
+ end
188
+
189
+ # take one radio button from each group
190
+ radio_groups.each_value do |g|
191
+ checked = g.select {|f| f.checked}
192
+
193
+ if checked.size == 1
194
+ f = checked.first
195
+ qval = proc_query(f)
196
+ query.push(*qval)
197
+ elsif checked.size > 1
198
+ raise "multiple radiobuttons are checked in the same group!"
199
+ end
200
+ end
201
+
202
+ @clicked_buttons.each { |b|
203
+ qval = proc_query(b)
204
+ query.push(*qval)
205
+ }
206
+ query
207
+ end
208
+
209
+ # This method adds a button to the query. If the form needs to be
210
+ # submitted with multiple buttons, pass each button to this method.
211
+ def add_button_to_query(button)
212
+ @clicked_buttons << button
213
+ end
214
+
215
+ # This method calculates the request data to be sent back to the server
216
+ # for this form, depending on if this is a regular post, get, or a
217
+ # multi-part post,
218
+ def request_data
219
+ query_params = build_query()
220
+ case @enctype.downcase
221
+ when /^multipart\/form-data/
222
+ boundary = rand_string(20)
223
+ @enctype = "multipart/form-data; boundary=#{boundary}"
224
+ params = []
225
+ query_params.each { |k,v| params << param_to_multipart(k, v) unless k.nil? }
226
+ @file_uploads.each { |f| params << file_to_multipart(f) }
227
+ params.collect { |p| "--#{boundary}\r\n#{p}" }.join('') +
228
+ "--#{boundary}--\r\n"
229
+ else
230
+ Mechanize::Util.build_query_string(query_params)
231
+ end
232
+ end
233
+
234
+ # Removes all fields with name +field_name+.
235
+ def delete_field!(field_name)
236
+ @fields.delete_if{ |f| f.name == field_name}
237
+ end
238
+
239
+ { :field => :fields,
240
+ :button => :buttons,
241
+ :file_upload => :file_uploads,
242
+ :radiobutton => :radiobuttons,
243
+ :checkbox => :checkboxes,
244
+ }.each do |singular,plural|
245
+ eval(<<-eomethod)
246
+ def #{plural}_with criteria = {}
247
+ criteria = {:name => criteria} if String === criteria
248
+ f = #{plural}.find_all do |thing|
249
+ criteria.all? { |k,v| v === thing.send(k) }
250
+ end
251
+ yield f if block_given?
252
+ f
253
+ end
254
+
255
+ def #{singular}_with criteria = {}
256
+ f = #{plural}_with(criteria).first
257
+ yield f if block_given?
258
+ f
259
+ end
260
+ alias :#{singular} :#{singular}_with
261
+ eomethod
262
+ end
263
+
264
+ private
265
+ def parse
266
+ @fields = []
267
+ @buttons = []
268
+ @file_uploads = []
269
+ @radiobuttons = []
270
+ @checkboxes = []
271
+
272
+ # Find all input tags
273
+ form_node.search('input').each do |node|
274
+ type = (node['type'] || 'text').downcase
275
+ name = node['name']
276
+ next if name.nil? && !(type == 'submit' || type =='button')
277
+ case type
278
+ when 'radio'
279
+ @radiobuttons << RadioButton.new(node['name'], node['value'], !!node['checked'], self)
280
+ when 'checkbox'
281
+ @checkboxes << CheckBox.new(node['name'], node['value'], !!node['checked'], self)
282
+ when 'file'
283
+ @file_uploads << FileUpload.new(node['name'], nil)
284
+ when 'submit'
285
+ @buttons << Button.new(node['name'], node['value'])
286
+ when 'button'
287
+ @buttons << Button.new(node['name'], node['value'])
288
+ when 'image'
289
+ @buttons << ImageButton.new(node['name'], node['value'])
290
+ else
291
+ @fields << Field.new(node['name'], node['value'] || '')
292
+ end
293
+ end
294
+
295
+ # Find all textarea tags
296
+ form_node.search('textarea').each do |node|
297
+ next if node['name'].nil?
298
+ @fields << Field.new(node['name'], node.inner_text)
299
+ end
300
+
301
+ # Find all select tags
302
+ form_node.search('select').each do |node|
303
+ next if node['name'].nil?
304
+ if node.has_attribute? 'multiple'
305
+ @fields << MultiSelectList.new(node['name'], node)
306
+ else
307
+ @fields << SelectList.new(node['name'], node)
308
+ end
309
+ end
310
+
311
+ # Find all submit button tags
312
+ # FIXME: what can I do with the reset buttons?
313
+ form_node.search('button').each do |node|
314
+ type = (node['type'] || 'submit').downcase
315
+ next if type == 'reset'
316
+ @buttons << Button.new(node['name'], node['value'])
317
+ end
318
+ end
319
+
320
+ def rand_string(len = 10)
321
+ chars = ("a".."z").to_a + ("A".."Z").to_a
322
+ string = ""
323
+ 1.upto(len) { |i| string << chars[rand(chars.size-1)] }
324
+ string
325
+ end
326
+
327
+ def mime_value_quote(str)
328
+ str.gsub(/(["\r\\])/){|s| '\\' + s}
329
+ end
330
+
331
+ def param_to_multipart(name, value)
332
+ return "Content-Disposition: form-data; name=\"" +
333
+ "#{mime_value_quote(name)}\"\r\n" +
334
+ "\r\n#{value}\r\n"
335
+ end
336
+
337
+ def file_to_multipart(file)
338
+ file_name = file.file_name ? ::File.basename(file.file_name) : ''
339
+ body = "Content-Disposition: form-data; name=\"" +
340
+ "#{mime_value_quote(file.name)}\"; " +
341
+ "filename=\"#{mime_value_quote(file_name)}\"\r\n" +
342
+ "Content-Transfer-Encoding: binary\r\n"
343
+
344
+ if file.file_data.nil? and ! file.file_name.nil?
345
+ file.file_data = ::File.open(file.file_name, "rb") { |f| f.read }
346
+ file.mime_type = WEBrick::HTTPUtils.mime_type(file.file_name,
347
+ WEBrick::HTTPUtils::DefaultMimeTypes)
348
+ end
349
+
350
+ if file.mime_type != nil
351
+ body << "Content-Type: #{file.mime_type}\r\n"
352
+ end
353
+
354
+ body <<
355
+ if file.file_data.respond_to? :read
356
+ "\r\n#{file.file_data.read}\r\n"
357
+ else
358
+ "\r\n#{file.file_data}\r\n"
359
+ end
360
+
361
+ body
362
+ end
363
+ end
364
+ end