diamond-mechanize 2.1

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 (154) hide show
  1. data/CHANGELOG.rdoc +718 -0
  2. data/EXAMPLES.rdoc +187 -0
  3. data/FAQ.rdoc +11 -0
  4. data/GUIDE.rdoc +163 -0
  5. data/LICENSE.rdoc +20 -0
  6. data/Manifest.txt +159 -0
  7. data/README.rdoc +64 -0
  8. data/Rakefile +49 -0
  9. data/lib/mechanize.rb +1079 -0
  10. data/lib/mechanize/content_type_error.rb +13 -0
  11. data/lib/mechanize/cookie.rb +232 -0
  12. data/lib/mechanize/cookie_jar.rb +194 -0
  13. data/lib/mechanize/download.rb +59 -0
  14. data/lib/mechanize/element_matcher.rb +36 -0
  15. data/lib/mechanize/file.rb +65 -0
  16. data/lib/mechanize/file_connection.rb +17 -0
  17. data/lib/mechanize/file_request.rb +26 -0
  18. data/lib/mechanize/file_response.rb +74 -0
  19. data/lib/mechanize/file_saver.rb +39 -0
  20. data/lib/mechanize/form.rb +543 -0
  21. data/lib/mechanize/form/button.rb +6 -0
  22. data/lib/mechanize/form/check_box.rb +12 -0
  23. data/lib/mechanize/form/field.rb +54 -0
  24. data/lib/mechanize/form/file_upload.rb +21 -0
  25. data/lib/mechanize/form/hidden.rb +3 -0
  26. data/lib/mechanize/form/image_button.rb +19 -0
  27. data/lib/mechanize/form/keygen.rb +34 -0
  28. data/lib/mechanize/form/multi_select_list.rb +94 -0
  29. data/lib/mechanize/form/option.rb +50 -0
  30. data/lib/mechanize/form/radio_button.rb +55 -0
  31. data/lib/mechanize/form/reset.rb +3 -0
  32. data/lib/mechanize/form/select_list.rb +44 -0
  33. data/lib/mechanize/form/submit.rb +3 -0
  34. data/lib/mechanize/form/text.rb +3 -0
  35. data/lib/mechanize/form/textarea.rb +3 -0
  36. data/lib/mechanize/headers.rb +23 -0
  37. data/lib/mechanize/history.rb +82 -0
  38. data/lib/mechanize/http.rb +8 -0
  39. data/lib/mechanize/http/agent.rb +1004 -0
  40. data/lib/mechanize/http/auth_challenge.rb +59 -0
  41. data/lib/mechanize/http/auth_realm.rb +31 -0
  42. data/lib/mechanize/http/content_disposition_parser.rb +188 -0
  43. data/lib/mechanize/http/www_authenticate_parser.rb +155 -0
  44. data/lib/mechanize/monkey_patch.rb +16 -0
  45. data/lib/mechanize/page.rb +440 -0
  46. data/lib/mechanize/page/base.rb +7 -0
  47. data/lib/mechanize/page/frame.rb +27 -0
  48. data/lib/mechanize/page/image.rb +30 -0
  49. data/lib/mechanize/page/label.rb +20 -0
  50. data/lib/mechanize/page/link.rb +98 -0
  51. data/lib/mechanize/page/meta_refresh.rb +68 -0
  52. data/lib/mechanize/parser.rb +173 -0
  53. data/lib/mechanize/pluggable_parsers.rb +144 -0
  54. data/lib/mechanize/redirect_limit_reached_error.rb +19 -0
  55. data/lib/mechanize/redirect_not_get_or_head_error.rb +21 -0
  56. data/lib/mechanize/response_code_error.rb +21 -0
  57. data/lib/mechanize/response_read_error.rb +27 -0
  58. data/lib/mechanize/robots_disallowed_error.rb +28 -0
  59. data/lib/mechanize/test_case.rb +663 -0
  60. data/lib/mechanize/unauthorized_error.rb +3 -0
  61. data/lib/mechanize/unsupported_scheme_error.rb +6 -0
  62. data/lib/mechanize/util.rb +101 -0
  63. data/test/data/htpasswd +1 -0
  64. data/test/data/server.crt +16 -0
  65. data/test/data/server.csr +12 -0
  66. data/test/data/server.key +15 -0
  67. data/test/data/server.pem +15 -0
  68. data/test/htdocs/alt_text.html +10 -0
  69. data/test/htdocs/bad_form_test.html +9 -0
  70. data/test/htdocs/button.jpg +0 -0
  71. data/test/htdocs/canonical_uri.html +9 -0
  72. data/test/htdocs/dir with spaces/foo.html +1 -0
  73. data/test/htdocs/empty_form.html +6 -0
  74. data/test/htdocs/file_upload.html +26 -0
  75. data/test/htdocs/find_link.html +41 -0
  76. data/test/htdocs/form_multi_select.html +16 -0
  77. data/test/htdocs/form_multival.html +37 -0
  78. data/test/htdocs/form_no_action.html +18 -0
  79. data/test/htdocs/form_no_input_name.html +16 -0
  80. data/test/htdocs/form_order_test.html +11 -0
  81. data/test/htdocs/form_select.html +16 -0
  82. data/test/htdocs/form_set_fields.html +14 -0
  83. data/test/htdocs/form_test.html +188 -0
  84. data/test/htdocs/frame_referer_test.html +10 -0
  85. data/test/htdocs/frame_test.html +30 -0
  86. data/test/htdocs/google.html +13 -0
  87. data/test/htdocs/index.html +6 -0
  88. data/test/htdocs/link with space.html +5 -0
  89. data/test/htdocs/meta_cookie.html +11 -0
  90. data/test/htdocs/no_title_test.html +6 -0
  91. data/test/htdocs/noindex.html +9 -0
  92. data/test/htdocs/rails_3_encoding_hack_form_test.html +27 -0
  93. data/test/htdocs/relative/tc_relative_links.html +21 -0
  94. data/test/htdocs/robots.html +8 -0
  95. data/test/htdocs/robots.txt +2 -0
  96. data/test/htdocs/tc_bad_charset.html +9 -0
  97. data/test/htdocs/tc_bad_links.html +5 -0
  98. data/test/htdocs/tc_base_link.html +8 -0
  99. data/test/htdocs/tc_blank_form.html +11 -0
  100. data/test/htdocs/tc_charset.html +6 -0
  101. data/test/htdocs/tc_checkboxes.html +19 -0
  102. data/test/htdocs/tc_encoded_links.html +5 -0
  103. data/test/htdocs/tc_field_precedence.html +11 -0
  104. data/test/htdocs/tc_follow_meta.html +8 -0
  105. data/test/htdocs/tc_form_action.html +48 -0
  106. data/test/htdocs/tc_links.html +19 -0
  107. data/test/htdocs/tc_meta_in_body.html +9 -0
  108. data/test/htdocs/tc_pretty_print.html +17 -0
  109. data/test/htdocs/tc_referer.html +16 -0
  110. data/test/htdocs/tc_relative_links.html +19 -0
  111. data/test/htdocs/tc_textarea.html +23 -0
  112. data/test/htdocs/test_click.html +11 -0
  113. data/test/htdocs/unusual______.html +5 -0
  114. data/test/test_mechanize.rb +1164 -0
  115. data/test/test_mechanize_cookie.rb +451 -0
  116. data/test/test_mechanize_cookie_jar.rb +483 -0
  117. data/test/test_mechanize_download.rb +43 -0
  118. data/test/test_mechanize_file.rb +61 -0
  119. data/test/test_mechanize_file_connection.rb +21 -0
  120. data/test/test_mechanize_file_request.rb +19 -0
  121. data/test/test_mechanize_file_saver.rb +21 -0
  122. data/test/test_mechanize_form.rb +875 -0
  123. data/test/test_mechanize_form_check_box.rb +38 -0
  124. data/test/test_mechanize_form_encoding.rb +114 -0
  125. data/test/test_mechanize_form_field.rb +63 -0
  126. data/test/test_mechanize_form_file_upload.rb +20 -0
  127. data/test/test_mechanize_form_image_button.rb +12 -0
  128. data/test/test_mechanize_form_keygen.rb +32 -0
  129. data/test/test_mechanize_form_multi_select_list.rb +84 -0
  130. data/test/test_mechanize_form_option.rb +55 -0
  131. data/test/test_mechanize_form_radio_button.rb +78 -0
  132. data/test/test_mechanize_form_select_list.rb +76 -0
  133. data/test/test_mechanize_form_textarea.rb +52 -0
  134. data/test/test_mechanize_headers.rb +35 -0
  135. data/test/test_mechanize_history.rb +103 -0
  136. data/test/test_mechanize_http_agent.rb +1225 -0
  137. data/test/test_mechanize_http_auth_challenge.rb +39 -0
  138. data/test/test_mechanize_http_auth_realm.rb +49 -0
  139. data/test/test_mechanize_http_content_disposition_parser.rb +118 -0
  140. data/test/test_mechanize_http_www_authenticate_parser.rb +146 -0
  141. data/test/test_mechanize_link.rb +80 -0
  142. data/test/test_mechanize_page.rb +118 -0
  143. data/test/test_mechanize_page_encoding.rb +182 -0
  144. data/test/test_mechanize_page_frame.rb +16 -0
  145. data/test/test_mechanize_page_link.rb +390 -0
  146. data/test/test_mechanize_page_meta_refresh.rb +127 -0
  147. data/test/test_mechanize_parser.rb +289 -0
  148. data/test/test_mechanize_pluggable_parser.rb +52 -0
  149. data/test/test_mechanize_redirect_limit_reached_error.rb +24 -0
  150. data/test/test_mechanize_redirect_not_get_or_head_error.rb +14 -0
  151. data/test/test_mechanize_subclass.rb +22 -0
  152. data/test/test_mechanize_util.rb +103 -0
  153. data/test/test_multi_select.rb +119 -0
  154. metadata +216 -0
@@ -0,0 +1,36 @@
1
+ module Mechanize::ElementMatcher
2
+
3
+ def elements_with singular, plural = "#{singular}s"
4
+ class_eval <<-CODE
5
+ def #{plural}_with criteria = {}
6
+ criteria = if String === criteria then
7
+ {:name => criteria}
8
+ else
9
+ criteria.map do |k, v|
10
+ k = :dom_id if k.to_sym == :id
11
+ k = :dom_class if k.to_sym == :class
12
+ [k, v]
13
+ end
14
+ end
15
+
16
+ f = #{plural}.find_all do |thing|
17
+ criteria.all? do |k,v|
18
+ v === thing.send(k)
19
+ end
20
+ end
21
+ yield f if block_given?
22
+ f
23
+ end
24
+
25
+ def #{singular}_with criteria = {}
26
+ f = #{plural}_with(criteria).first
27
+ yield f if block_given?
28
+ f
29
+ end
30
+
31
+ alias :#{singular} :#{singular}_with
32
+ CODE
33
+ end
34
+
35
+ end
36
+
@@ -0,0 +1,65 @@
1
+ ##
2
+ # This is the base class for the Pluggable Parsers. If Mechanize cannot find
3
+ # an appropriate class to use for the content type, this class will be used.
4
+ # For example, if you download an image/jpeg, Mechanize will not know how to
5
+ # parse it, so this class will be instantiated.
6
+ #
7
+ # This is a good class to use as the base class for building your own
8
+ # pluggable parsers.
9
+ #
10
+ # == Example
11
+ #
12
+ # require 'mechanize'
13
+ #
14
+ # agent = Mechanize.new
15
+ # agent.get('http://example.com/foo.jpg').class #=> Mechanize::File
16
+
17
+ class Mechanize::File
18
+
19
+ include Mechanize::Parser
20
+
21
+ ##
22
+ # The HTTP response body, the raw file contents
23
+
24
+ attr_accessor :body
25
+
26
+ ##
27
+ # The filename for this file based on the content-disposition of the
28
+ # response or the basename of the URL
29
+
30
+ attr_accessor :filename
31
+
32
+ alias content body
33
+
34
+ ##
35
+ # Creates a new file retrieved from the given +uri+ and +response+ object.
36
+ # The +body+ is the HTTP response body and +code+ is the HTTP status.
37
+
38
+ def initialize uri = nil, response = nil, body = nil, code = nil
39
+ @uri = uri
40
+ @body = body
41
+ @code = code
42
+
43
+ @full_path = false unless defined? @full_path
44
+
45
+ fill_header response
46
+ extract_filename
47
+
48
+ yield self if block_given?
49
+ end
50
+
51
+ ##
52
+ # Use this method to save the content of this object to +filename+
53
+
54
+ def save filename = nil
55
+ filename = find_free_name filename
56
+
57
+ open filename, 'wb' do |f|
58
+ f.write body
59
+ end
60
+ end
61
+
62
+ alias save_as save
63
+
64
+ end
65
+
@@ -0,0 +1,17 @@
1
+ ##
2
+ # Wrapper to make a file URI work like an http URI
3
+
4
+ class Mechanize::FileConnection
5
+
6
+ @instance = nil
7
+
8
+ def self.new *a
9
+ @instance ||= super
10
+ end
11
+
12
+ def request uri, request
13
+ yield Mechanize::FileResponse.new Mechanize::Util.uri_unescape uri.path
14
+ end
15
+
16
+ end
17
+
@@ -0,0 +1,26 @@
1
+ ##
2
+ # A wrapper for a file URI that makes a request that works like a
3
+ # Net::HTTPRequest
4
+
5
+ class Mechanize::FileRequest
6
+
7
+ attr_accessor :uri
8
+
9
+ def initialize uri
10
+ @uri = uri
11
+ end
12
+
13
+ def add_field *a
14
+ end
15
+
16
+ alias []= add_field
17
+
18
+ def path
19
+ @uri.path
20
+ end
21
+
22
+ def each_header
23
+ end
24
+
25
+ end
26
+
@@ -0,0 +1,74 @@
1
+ ##
2
+ # Fake response for dealing with file:/// requests
3
+
4
+ class Mechanize::FileResponse
5
+ def initialize(file_path)
6
+ @file_path = file_path
7
+ end
8
+
9
+ def read_body
10
+ raise Mechanize::ResponseCodeError, self unless File.exist? @file_path
11
+
12
+ if directory?
13
+ yield dir_body
14
+ else
15
+ open @file_path, 'rb' do |io|
16
+ yield io.read
17
+ end
18
+ end
19
+ end
20
+
21
+ def code
22
+ File.exist?(@file_path) ? 200 : 404
23
+ end
24
+
25
+ def content_length
26
+ return dir_body.length if directory?
27
+ File.exist?(@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
+ def http_version
49
+ '0'
50
+ end
51
+
52
+ def message
53
+ File.exist?(@file_path) ? 'OK' : 'Not Found'
54
+ end
55
+
56
+ private
57
+
58
+ def dir_body
59
+ body = %w[<html><body>]
60
+ body.concat Dir[File.join(@file_path, '*')].map { |f|
61
+ "<a href=\"file://#{f}\">#{File.basename(f)}</a>"
62
+ }
63
+ body << %w[</body></html>]
64
+
65
+ body = body.join "\n"
66
+ body.force_encoding Encoding::BINARY if body.respond_to? :force_encoding
67
+ body
68
+ end
69
+
70
+ def directory?
71
+ File.directory?(@file_path)
72
+ end
73
+ end
74
+
@@ -0,0 +1,39 @@
1
+ ##
2
+ # This is a pluggable parser that automatically saves every file it
3
+ # encounters. It saves the files as a tree, reflecting the host and file
4
+ # path.
5
+ #
6
+ # == Example
7
+ #
8
+ # This example saves all .pdf files
9
+ #
10
+ # require 'mechanize'
11
+ #
12
+ # agent = Mechanize.new
13
+ # agent.pluggable_parser.pdf = Mechanize::FileSaver
14
+ # agent.get('http://example.com/foo.pdf')
15
+ #
16
+ # Dir['example.com/*'] # => foo.pdf
17
+
18
+ class Mechanize::FileSaver < Mechanize::Download
19
+
20
+ attr_reader :filename
21
+
22
+ def initialize uri = nil, response = nil, body_io = nil, code = nil
23
+ @full_path = true
24
+
25
+ super
26
+
27
+ save @filename
28
+ end
29
+
30
+ ##
31
+ # The save_as alias is provided for backwards compatibility with mechanize
32
+ # 2.0. It will be removed in mechanize 3.
33
+ #--
34
+ # TODO remove in mechanize 3
35
+
36
+ alias save_as save
37
+
38
+ end
39
+
@@ -0,0 +1,543 @@
1
+ require 'mechanize/element_matcher'
2
+
3
+ # This class encapsulates a form parsed out of an HTML page. Each type of
4
+ # input fields available in a form can be accessed through this object.
5
+ #
6
+ # == Examples
7
+ #
8
+ # Find a form and print out its fields
9
+ #
10
+ # form = page.forms.first # => Mechanize::Form
11
+ # form.fields.each { |f| puts f.name }
12
+ #
13
+ # Set the input field 'name' to "Aaron"
14
+ #
15
+ # form['name'] = 'Aaron'
16
+ # puts form['name']
17
+
18
+ class Mechanize::Form
19
+
20
+ extend Mechanize::ElementMatcher
21
+
22
+ attr_accessor :method, :action, :name
23
+
24
+ attr_reader :fields, :buttons, :file_uploads, :radiobuttons, :checkboxes
25
+
26
+ # Content-Type for form data (i.e. application/x-www-form-urlencoded)
27
+ attr_accessor :enctype
28
+
29
+ # Character encoding of form data (i.e. UTF-8)
30
+ attr_accessor :encoding
31
+
32
+ # When true, character encoding errors will never be never raised on form
33
+ # submission. Default is false
34
+ attr_accessor :ignore_encoding_error
35
+
36
+ alias :elements :fields
37
+
38
+ attr_reader :form_node
39
+ attr_reader :page
40
+
41
+ def initialize(node, mech=nil, page=nil)
42
+ @enctype = node['enctype'] || 'application/x-www-form-urlencoded'
43
+ @form_node = node
44
+ @action = Mechanize::Util.html_unescape(node['action'])
45
+ @method = (node['method'] || 'GET').upcase
46
+ @name = node['name']
47
+ @clicked_buttons = []
48
+ @page = page
49
+ @mech = mech
50
+
51
+ @encoding = node['accept-charset'] || (page && page.encoding) || nil
52
+ @ignore_encoding_error = false
53
+ parse
54
+ end
55
+
56
+ # Returns whether or not the form contains a field with +field_name+
57
+ def has_field?(field_name)
58
+ fields.find { |f| f.name == field_name }
59
+ end
60
+
61
+ alias :has_key? :has_field?
62
+
63
+ def has_value?(value)
64
+ fields.find { |f| f.value == value }
65
+ end
66
+
67
+ def keys; fields.map { |f| f.name }; end
68
+
69
+ def values; fields.map { |f| f.value }; end
70
+
71
+ def submits ; @submits ||= buttons.select { |f| f.class == Submit }; end
72
+ def resets ; @resets ||= buttons.select { |f| f.class == Reset }; end
73
+ def texts ; @texts ||= fields.select { |f| f.class == Text }; end
74
+ def hiddens ; @hiddens ||= fields.select { |f| f.class == Hidden }; end
75
+ def textareas; @textareas ||= fields.select { |f| f.class == Textarea }; end
76
+ def keygens ; @keygens ||= fields.select { |f| f.class == Keygen }; end
77
+
78
+ def submit_button?(button_name) submits.find{|f| f.name == button_name}; end
79
+ def reset_button?(button_name) resets.find{|f| f.name == button_name}; end
80
+ def text_field?(field_name) texts.find{|f| f.name == field_name}; end
81
+ def hidden_field?(field_name) hiddens.find{|f| f.name == field_name}; end
82
+ def textarea_field?(field_name) textareas.find{|f| f.name == field_name}; end
83
+
84
+ # This method is a shortcut to get form's DOM id.
85
+ # Common usage:
86
+ # page.form_with(:dom_id => "foorm")
87
+ # Note that you can also use +:id+ to get to this method:
88
+ # page.form_with(:id => "foorm")
89
+ def dom_id
90
+ form_node['id']
91
+ end
92
+
93
+ # This method is a shortcut to get form's DOM class.
94
+ # Common usage:
95
+ # page.form_with(:dom_class => "foorm")
96
+ # Note that you can also use +:class+ to get to this method:
97
+ # page.form_with(:class => "foorm")
98
+ def dom_class
99
+ form_node['class']
100
+ end
101
+
102
+ # Add a field with +field_name+ and +value+
103
+ def add_field!(field_name, value = nil)
104
+ fields << Field.new({'name' => field_name}, value)
105
+ end
106
+
107
+ ##
108
+ # This method sets multiple fields on the form. It takes a list of +fields+
109
+ # which are name, value pairs.
110
+ #
111
+ # If there is more than one field found with the same name, this method will
112
+ # set the first one found. If you want to set the value of a duplicate
113
+ # field, use a value which is a Hash with the key as the index in to the
114
+ # form. The index is zero based.
115
+ #
116
+ # For example, to set the second field named 'foo', you could do the
117
+ # following:
118
+ #
119
+ # form.set_fields :foo => { 1 => 'bar' }
120
+
121
+ def set_fields fields = {}
122
+ fields.each do |name, v|
123
+ case v
124
+ when Hash
125
+ v.each do |index, value|
126
+ self.fields_with(:name => name.to_s)[index].value = value
127
+ end
128
+ else
129
+ value = nil
130
+ index = 0
131
+
132
+ [v].flatten.each do |val|
133
+ index = val.to_i if value
134
+ value = val unless value
135
+ end
136
+
137
+ self.fields_with(:name => name.to_s)[index].value = value
138
+ end
139
+ end
140
+ end
141
+
142
+ # Fetch the value of the first input field with the name passed in
143
+ # ==Example
144
+ # Fetch the value set in the input field 'name'
145
+ # puts form['name']
146
+ def [](field_name)
147
+ f = field(field_name)
148
+ f && f.value
149
+ end
150
+
151
+ # Set the value of the first input field with the name passed in
152
+ # ==Example
153
+ # Set the value in the input field 'name' to "Aaron"
154
+ # form['name'] = 'Aaron'
155
+ def []=(field_name, value)
156
+ f = field(field_name)
157
+ if f
158
+ f.value = value
159
+ else
160
+ add_field!(field_name, value)
161
+ end
162
+ end
163
+
164
+ # Treat form fields like accessors.
165
+ def method_missing(meth, *args)
166
+ method = meth.to_s.gsub(/=$/, '')
167
+
168
+ if field(method)
169
+ return field(method).value if args.empty?
170
+ return field(method).value = args[0]
171
+ end
172
+
173
+ super
174
+ end
175
+
176
+ # Submit this form with the button passed in
177
+ def submit button=nil, headers = {}
178
+ @mech.submit(self, button, headers)
179
+ end
180
+
181
+ # Submit form using +button+. Defaults
182
+ # to the first button.
183
+ def click_button(button = buttons.first)
184
+ submit(button)
185
+ end
186
+
187
+ # This method is sub-method of build_query.
188
+ # It converts charset of query value of fields into expected one.
189
+ def proc_query(field)
190
+ return unless field.query_value
191
+ field.query_value.map{|(name, val)|
192
+ [from_native_charset(name), from_native_charset(val.to_s)]
193
+ }
194
+ end
195
+ private :proc_query
196
+
197
+ def from_native_charset str
198
+ Mechanize::Util.from_native_charset(str, encoding, @ignore_encoding_error,
199
+ @mech && @mech.log)
200
+ end
201
+ private :from_native_charset
202
+
203
+ # This method builds an array of arrays that represent the query
204
+ # parameters to be used with this form. The return value can then
205
+ # be used to create a query string for this form.
206
+ def build_query(buttons = [])
207
+ query = []
208
+ @mech.log.info("form encoding: #{encoding}") if @mech && @mech.log
209
+
210
+ successful_controls = []
211
+
212
+ (fields + checkboxes).sort.each do |f|
213
+ case f
214
+ when Mechanize::Form::CheckBox
215
+ if f.checked
216
+ successful_controls << f
217
+ end
218
+ when Mechanize::Form::Field
219
+ successful_controls << f
220
+ end
221
+ end
222
+
223
+ radio_groups = {}
224
+ radiobuttons.each do |f|
225
+ fname = from_native_charset(f.name)
226
+ radio_groups[fname] ||= []
227
+ radio_groups[fname] << f
228
+ end
229
+
230
+ # take one radio button from each group
231
+ radio_groups.each_value do |g|
232
+ checked = g.select {|f| f.checked}
233
+
234
+ if checked.size == 1
235
+ f = checked.first
236
+ successful_controls << f
237
+ elsif checked.size > 1
238
+ raise Mechanize::Error,
239
+ "multiple radiobuttons are checked in the same group!"
240
+ end
241
+ end
242
+
243
+ @clicked_buttons.each { |b|
244
+ successful_controls << b
245
+ }
246
+
247
+ successful_controls.sort.each do |ctrl| # DOM order
248
+ qval = proc_query(ctrl)
249
+ query.push(*qval)
250
+ end
251
+
252
+ query
253
+ end
254
+
255
+ # This method adds a button to the query. If the form needs to be
256
+ # submitted with multiple buttons, pass each button to this method.
257
+ def add_button_to_query(button)
258
+ @clicked_buttons << button
259
+ end
260
+
261
+ # This method calculates the request data to be sent back to the server
262
+ # for this form, depending on if this is a regular post, get, or a
263
+ # multi-part post,
264
+ def request_data
265
+ query_params = build_query()
266
+
267
+ case @enctype.downcase
268
+ when /^multipart\/form-data/
269
+ boundary = rand_string(20)
270
+ @enctype = "multipart/form-data; boundary=#{boundary}"
271
+
272
+ params = query_params.map do |k,v|
273
+ param_to_multipart(k, v) if k
274
+ end.compact
275
+
276
+ params.concat @file_uploads.map { |f| file_to_multipart(f) }
277
+
278
+ params.map do |part|
279
+ part.force_encoding('ASCII-8BIT') if part.respond_to? :force_encoding
280
+ "--#{boundary}\r\n#{part}"
281
+ end.join('') +
282
+ "--#{boundary}--\r\n"
283
+ else
284
+ Mechanize::Util.build_query_string(query_params)
285
+ end
286
+ end
287
+
288
+ # Removes all fields with name +field_name+.
289
+ def delete_field!(field_name)
290
+ @fields.delete_if{ |f| f.name == field_name}
291
+ end
292
+
293
+ ##
294
+ # :method: field_with(criteria)
295
+ #
296
+ # Find one field that matches +criteria+
297
+ # Example:
298
+ # form.field_with(:id => "exact_field_id").value = 'hello'
299
+
300
+ ##
301
+ # :method: fields_with(criteria)
302
+ #
303
+ # Find all fields that match +criteria+
304
+ # Example:
305
+ # form.fields_with(:value => /foo/).each do |field|
306
+ # field.value = 'hello!'
307
+ # end
308
+
309
+ elements_with :field
310
+
311
+ ##
312
+ # :method: button_with(criteria)
313
+ #
314
+ # Find one button that matches +criteria+
315
+ # Example:
316
+ # form.button_with(:value => /submit/).value = 'hello'
317
+
318
+ ##
319
+ # :method: buttons_with(criteria)
320
+ #
321
+ # Find all buttons that match +criteria+
322
+ # Example:
323
+ # form.buttons_with(:value => /submit/).each do |button|
324
+ # button.value = 'hello!'
325
+ # end
326
+
327
+ elements_with :button
328
+
329
+ ##
330
+ # :method: file_upload_with(criteria)
331
+ #
332
+ # Find one file upload field that matches +criteria+
333
+ # Example:
334
+ # form.file_upload_with(:file_name => /picture/).value = 'foo'
335
+
336
+ ##
337
+ # :method: file_uploads_with(criteria)
338
+ #
339
+ # Find all file upload fields that match +criteria+
340
+ # Example:
341
+ # form.file_uploads_with(:file_name => /picutre/).each do |field|
342
+ # field.value = 'foo!'
343
+ # end
344
+
345
+ elements_with :file_upload
346
+
347
+ ##
348
+ # :method: radiobutton_with(criteria)
349
+ #
350
+ # Find one radio button that matches +criteria+
351
+ # Example:
352
+ # form.radiobutton_with(:name => /woo/).check
353
+
354
+ ##
355
+ # :method: radiobuttons_with(criteria)
356
+ #
357
+ # Find all radio buttons that match +criteria+
358
+ # Example:
359
+ # form.radiobuttons_with(:name => /woo/).each do |field|
360
+ # field.check
361
+ # end
362
+
363
+ elements_with :radiobutton
364
+
365
+ ##
366
+ # :method: checkbox_with(criteria)
367
+ #
368
+ # Find one checkbox that matches +criteria+
369
+ # Example:
370
+ # form.checkbox_with(:name => /woo/).check
371
+
372
+ ##
373
+ # :method: checkboxes_with(criteria)
374
+ #
375
+ # Find all checkboxes that match +criteria+
376
+ # Example:
377
+ # form.checkboxes_with(:name => /woo/).each do |field|
378
+ # field.check
379
+ # end
380
+
381
+ elements_with :checkbox, :checkboxes
382
+
383
+ def pretty_print(q) # :nodoc:
384
+ q.object_group(self) {
385
+ q.breakable; q.group(1, '{name', '}') { q.breakable; q.pp name }
386
+ q.breakable; q.group(1, '{method', '}') { q.breakable; q.pp method }
387
+ q.breakable; q.group(1, '{action', '}') { q.breakable; q.pp action }
388
+ q.breakable; q.group(1, '{fields', '}') {
389
+ fields.each do |field|
390
+ q.breakable
391
+ q.pp field
392
+ end
393
+ }
394
+ q.breakable; q.group(1, '{radiobuttons', '}') {
395
+ radiobuttons.each { |b| q.breakable; q.pp b }
396
+ }
397
+ q.breakable; q.group(1, '{checkboxes', '}') {
398
+ checkboxes.each { |b| q.breakable; q.pp b }
399
+ }
400
+ q.breakable; q.group(1, '{file_uploads', '}') {
401
+ file_uploads.each { |b| q.breakable; q.pp b }
402
+ }
403
+ q.breakable; q.group(1, '{buttons', '}') {
404
+ buttons.each { |b| q.breakable; q.pp b }
405
+ }
406
+ }
407
+ end
408
+
409
+ alias inspect pretty_inspect # :nodoc:
410
+
411
+ private
412
+
413
+ def parse
414
+ @fields = []
415
+ @buttons = []
416
+ @file_uploads = []
417
+ @radiobuttons = []
418
+ @checkboxes = []
419
+
420
+ # Find all input tags
421
+ form_node.search('input').each do |node|
422
+ type = (node['type'] || 'text').downcase
423
+ name = node['name']
424
+ next if name.nil? && !%w[submit button image].include?(type)
425
+ case type
426
+ when 'radio'
427
+ @radiobuttons << RadioButton.new(node, self)
428
+ when 'checkbox'
429
+ @checkboxes << CheckBox.new(node, self)
430
+ when 'file'
431
+ @file_uploads << FileUpload.new(node, nil)
432
+ when 'submit'
433
+ @buttons << Submit.new(node)
434
+ when 'button'
435
+ @buttons << Button.new(node)
436
+ when 'reset'
437
+ @buttons << Reset.new(node)
438
+ when 'image'
439
+ @buttons << ImageButton.new(node)
440
+ when 'hidden'
441
+ @fields << Hidden.new(node, node['value'] || '')
442
+ when 'text'
443
+ @fields << Text.new(node, node['value'] || '')
444
+ when 'textarea'
445
+ @fields << Textarea.new(node, node['value'] || '')
446
+ else
447
+ @fields << Field.new(node, node['value'] || '')
448
+ end
449
+ end
450
+
451
+ # Find all textarea tags
452
+ form_node.search('textarea').each do |node|
453
+ next unless node['name']
454
+ @fields << Textarea.new(node, node.inner_text)
455
+ end
456
+
457
+ # Find all select tags
458
+ form_node.search('select').each do |node|
459
+ next unless node['name']
460
+ if node.has_attribute? 'multiple'
461
+ @fields << MultiSelectList.new(node)
462
+ else
463
+ @fields << SelectList.new(node)
464
+ end
465
+ end
466
+
467
+ # Find all submit button tags
468
+ # FIXME: what can I do with the reset buttons?
469
+ form_node.search('button').each do |node|
470
+ type = (node['type'] || 'submit').downcase
471
+ next if type == 'reset'
472
+ @buttons << Button.new(node)
473
+ end
474
+
475
+ # Find all keygen tags
476
+ form_node.search('keygen').each do |node|
477
+ @fields << Keygen.new(node, node['value'] || '')
478
+ end
479
+ end
480
+
481
+ def rand_string(len = 10)
482
+ chars = ("a".."z").to_a + ("A".."Z").to_a
483
+ string = ""
484
+ 1.upto(len) { |i| string << chars[rand(chars.size-1)] }
485
+ string
486
+ end
487
+
488
+ def mime_value_quote(str)
489
+ str.gsub(/(["\r\\])/){|s| '\\' + s}
490
+ end
491
+
492
+ def param_to_multipart(name, value)
493
+ return "Content-Disposition: form-data; name=\"" +
494
+ "#{mime_value_quote(name)}\"\r\n" +
495
+ "\r\n#{value}\r\n"
496
+ end
497
+
498
+ def file_to_multipart(file)
499
+ file_name = file.file_name ? ::File.basename(file.file_name) : ''
500
+ body = "Content-Disposition: form-data; name=\"" +
501
+ "#{mime_value_quote(file.name)}\"; " +
502
+ "filename=\"#{mime_value_quote(file_name)}\"\r\n" +
503
+ "Content-Transfer-Encoding: binary\r\n"
504
+
505
+ if file.file_data.nil? and file.file_name
506
+ file.file_data = open(file.file_name, "rb") { |f| f.read }
507
+ file.mime_type =
508
+ WEBrick::HTTPUtils.mime_type(file.file_name,
509
+ WEBrick::HTTPUtils::DefaultMimeTypes)
510
+ end
511
+
512
+ if file.mime_type
513
+ body << "Content-Type: #{file.mime_type}\r\n"
514
+ end
515
+
516
+ body <<
517
+ if file.file_data.respond_to? :read
518
+ "\r\n#{file.file_data.read}\r\n"
519
+ else
520
+ "\r\n#{file.file_data}\r\n"
521
+ end
522
+
523
+ body
524
+ end
525
+
526
+ end
527
+
528
+ require 'mechanize/form/field'
529
+ require 'mechanize/form/button'
530
+ require 'mechanize/form/hidden'
531
+ require 'mechanize/form/text'
532
+ require 'mechanize/form/textarea'
533
+ require 'mechanize/form/submit'
534
+ require 'mechanize/form/reset'
535
+ require 'mechanize/form/file_upload'
536
+ require 'mechanize/form/keygen'
537
+ require 'mechanize/form/image_button'
538
+ require 'mechanize/form/multi_select_list'
539
+ require 'mechanize/form/option'
540
+ require 'mechanize/form/radio_button'
541
+ require 'mechanize/form/check_box'
542
+ require 'mechanize/form/select_list'
543
+