mechanize 0.4.7 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of mechanize might be problematic. Click here for more details.

Files changed (48) hide show
  1. data/CHANGELOG +17 -0
  2. data/EXAMPLES +23 -44
  3. data/NOTES +49 -0
  4. data/lib/mechanize.rb +95 -80
  5. data/lib/mechanize/cookie.rb +147 -148
  6. data/lib/mechanize/cookie.rb.rej +16 -0
  7. data/lib/mechanize/errors.rb +29 -0
  8. data/lib/mechanize/form.rb +211 -186
  9. data/lib/mechanize/form_elements.rb +31 -71
  10. data/lib/mechanize/list.rb +34 -0
  11. data/lib/mechanize/mech_version.rb +3 -1
  12. data/lib/mechanize/module.rb +1 -1
  13. data/lib/mechanize/page.rb +162 -180
  14. data/lib/mechanize/page_elements.rb +53 -40
  15. data/lib/mechanize/parsing.rb +11 -3
  16. data/lib/mechanize/pluggable_parsers.rb +147 -0
  17. data/test/data/server.crt +14 -0
  18. data/test/data/server.csr +11 -0
  19. data/test/data/server.key +18 -0
  20. data/test/data/server.pem +15 -0
  21. data/test/htdocs/no_title_test.html +6 -0
  22. data/test/parse.rb +39 -0
  23. data/test/proxy.rb +30 -0
  24. data/test/server.rb +2 -0
  25. data/test/servlets.rb +8 -0
  26. data/test/ssl_server.rb +49 -0
  27. data/test/tc_authenticate.rb +8 -6
  28. data/test/tc_cookie_class.rb +28 -18
  29. data/test/tc_cookie_jar.rb +88 -27
  30. data/test/tc_cookies.rb +41 -44
  31. data/test/tc_errors.rb +9 -23
  32. data/test/tc_forms.rb +36 -32
  33. data/test/tc_frames.rb +6 -4
  34. data/test/tc_links.rb +7 -6
  35. data/test/tc_mech.rb +43 -46
  36. data/test/tc_page.rb +24 -0
  37. data/test/tc_pluggable_parser.rb +103 -0
  38. data/test/tc_post_form.rb +41 -0
  39. data/test/tc_proxy.rb +25 -0
  40. data/test/tc_response_code.rb +13 -10
  41. data/test/tc_save_file.rb +25 -0
  42. data/test/tc_ssl_server.rb +27 -0
  43. data/test/tc_upload.rb +8 -6
  44. data/test/tc_watches.rb +5 -2
  45. data/test/test_includes.rb +3 -3
  46. data/test/ts_mech.rb +11 -2
  47. metadata +100 -86
  48. data/test/tc_filter.rb +0 -34
@@ -1,175 +1,174 @@
1
- require 'date'
1
+ require 'yaml'
2
+ require 'time'
2
3
 
3
4
  module WWW
5
+ class Mechanize
4
6
  # This class is used to represent an HTTP Cookie.
5
- class Cookie
6
- attr_reader :name, :value, :path, :domain, :expires, :secure
7
- def initialize(cookie)
8
- @name = cookie[:name]
9
- @value = cookie[:value]
10
- @path = cookie[:path]
11
- @domain = cookie[:domain]
12
- @expires = cookie[:expires]
13
- @secure = cookie[:secure]
14
- @string = "#{cookie[:name]}=#{cookie[:value]}"
15
- end
16
-
17
- def Cookie::parse(uri, raw_cookie, &block)
18
- esc = raw_cookie.gsub(/(expires=[^,]*),([^;]*(;|$))/i) { "#{$1}#{$2}" }
19
- esc.split(/,/).each do |cookie_text|
20
- cookie_values = Hash.new
21
- cookie = Hash.new
22
- cookie_text.split(/; ?/).each do |data|
23
- name, value = data.split('=', 2)
24
- next unless name
25
- cookie[name.strip] = value
26
- end
27
-
28
- cookie_values[:path] = cookie.delete(
29
- cookie.keys.find { |k| k.downcase == "path" }
30
- ) || uri.path
7
+ class Cookie
8
+ attr_reader :name, :value, :path, :domain, :expires, :secure
9
+ def initialize(cookie)
10
+ @name = cookie[:name]
11
+ @value = cookie[:value]
12
+ @path = cookie[:path]
13
+ @domain = cookie[:domain]
14
+ @expires = cookie[:expires]
15
+ @secure = cookie[:secure]
16
+ @string = "#{cookie[:name]}=#{cookie[:value]}"
17
+ end
18
+
19
+ def Cookie::parse(uri, raw_cookie, &block)
20
+ esc = raw_cookie.gsub(/(expires=[^,]*),([^;]*(;|$))/i) { "#{$1}#{$2}" }
21
+ esc.split(/,/).each do |cookie_text|
22
+ cookie_values = Hash.new
23
+ cookie = Hash.new
24
+ cookie_text.split(/; ?/).each do |data|
25
+ name, value = data.split('=', 2)
26
+ next unless name
27
+ cookie[name.strip] = value
28
+ end
29
+
30
+ cookie_values[:path] = cookie.delete(
31
+ cookie.keys.find { |k| k.downcase == "path" } ) || uri.path
31
32
 
32
- expires_key = cookie.keys.find { |k| k.downcase == "expires" }
33
- if expires_key
34
- time = nil
35
- expires_val = cookie.delete(expires_key)
33
+ expires_key = cookie.keys.find { |k| k.downcase == "expires" }
34
+ if expires_key
35
+ time = nil
36
+ expires_val = cookie.delete(expires_key)
36
37
 
37
- # First lets try dates with timezones
38
- [ '%A %d-%b-%y %T %Z',
39
- '%a %d-%b-%Y %T %Z',
40
- '%a %d %b %Y %T %Z',
41
- '%d %b %y %H:%M %Z', # 14 Apr 89 03:20 GMT
42
- '%a %d %b %y %H:%M %Z', # Fri, 17 Mar 89 4:01 GMT
43
- '%a %b %d %H:%M %Z %Y', # Mon Jan 16 16:12 PDT 1989
44
- '%d %b %Y %H:%M-%Z (%A)', # 6 May 1992 16:41-JST (Wednesday)
45
- '%y-%m-%d %T %Z', # 95-06-08 19:32:48 EDT
46
- ].each { |fmt|
38
+ # If we can't parse the time, set it to the current time
47
39
  begin
48
- time = DateTime.strptime(expires_val, fmt)
49
- rescue ArgumentError => er
50
- else
51
- break
40
+ time = Time::parse(expires_val)
41
+ rescue
42
+ time = Time.now
52
43
  end
53
- }
54
44
 
55
- # If it didn't have a timezone, we'll assume GMT, like Mozilla does
56
- if time.nil?
57
- [
58
- '%d %b %y %T %Z', # 14 Apr 89 03:20:12
59
- '%a %d %b %y %T %Z', # Fri, 17 Mar 89 4:01:33
60
- #'%d-%b-%Y %H:%M:%S.%N %Z', # 22-AUG-1993 10:59:12.82
61
- '%d-%b-%Y %H:%M%P %Z', # 22-AUG-1993 10:59pm
62
- '%d-%b-%Y %H:%M %p %Z', # 22-AUG-1993 12:59 PM
63
- #'%A %B %d %Y %H:%M %p', # Friday, August 04, 1995 3:54 PM
64
- '%x %I:%M:%S %p %Z', # 06/21/95 04:24:34 PM
65
- '%d/%m/%y %H:%M %Z', # 20/06/95 21:07
66
- ].each { |fmt|
67
- begin
68
- time = DateTime.strptime("#{expires_val} GMT", fmt)
69
- rescue ArgumentError => er
70
- else
71
- break
72
- end
73
- }
45
+ # If we couldn't parse it, set it to the current time
46
+ time = Time.now if time == nil
47
+ cookie_values[:expires] = time
74
48
  end
75
49
 
76
- # If we couldn't parse it, set it to the current time
77
- time = DateTime.now if time == nil
78
- cookie_values[:expires] = time
79
- end
80
-
81
- secure_key = cookie.keys.find { |k| k.downcase == "secure" }
82
- if secure_key
83
- cookie_values[:secure] = true
84
- cookie.delete(secure_key)
85
- else
86
- cookie_values[:secure] = false
87
- end
88
-
89
- # Set the domain name of the cookie
90
- domain_key = cookie.keys.find { |k| k.downcase == "domain" }
91
- if domain_key
92
- domain = cookie.delete(domain_key)
93
- domain.sub!(/^\./, '')
50
+ secure_key = cookie.keys.find { |k| k.downcase == "secure" }
51
+ if secure_key
52
+ cookie_values[:secure] = true
53
+ cookie.delete(secure_key)
54
+ else
55
+ cookie_values[:secure] = false
56
+ end
57
+
58
+ # Set the domain name of the cookie
59
+ domain_key = cookie.keys.find { |k| k.downcase == "domain" }
60
+ if domain_key
61
+ domain = cookie.delete(domain_key)
62
+ domain.sub!(/^\./, '')
94
63
 
95
- # Reject cookies not for this domain
96
- next unless uri.host =~ /#{domain}$/
97
- cookie_values[:domain] = domain
98
- else
99
- cookie_values[:domain] = uri.host
100
- end
64
+ # Reject cookies not for this domain
65
+ next unless uri.host =~ /#{domain}$/
66
+ cookie_values[:domain] = domain
67
+ else
68
+ cookie_values[:domain] = uri.host
69
+ end
101
70
 
102
- # Delete the http only option
103
- # http://msdn.microsoft.com/workshop/author/dhtml/httponly_cookies.asp
104
- http_only = cookie.keys.find { |k| k.downcase == 'httponly' }
105
- cookie.delete(http_only) if http_only
71
+ # Delete the http only option
72
+ # http://msdn.microsoft.com/workshop/author/dhtml/httponly_cookies.asp
73
+ http_only = cookie.keys.find { |k| k.downcase == 'httponly' }
74
+ cookie.delete(http_only) if http_only
106
75
 
107
- cookie.each { |k,v|
108
- cookie_values[:name] = k.strip
109
- cookie_values[:value] = v.strip
110
- }
111
- yield Cookie.new(cookie_values)
76
+ cookie.each { |k,v|
77
+ cookie_values[:name] = k.strip
78
+ cookie_values[:value] = v.strip
79
+ }
80
+ yield Cookie.new(cookie_values)
81
+ end
82
+ end
83
+
84
+ def to_s
85
+ @string
112
86
  end
113
87
  end
114
-
115
- def to_s
116
- @string
117
- end
118
- end
119
88
 
120
- # This class is used to manage the Cookies that have been returned from
121
- # any particular website.
122
- class CookieJar
123
- attr_accessor :jar
124
- def initialize
125
- @jar = {}
126
- end
127
-
128
- # Add a cookie to the Jar.
129
- def add(cookie)
130
- unless @jar.has_key?(cookie.domain)
131
- @jar[cookie.domain] = Hash.new
89
+ # This class is used to manage the Cookies that have been returned from
90
+ # any particular website.
91
+ class CookieJar
92
+ attr_reader :jar
93
+
94
+ def initialize
95
+ @jar = {}
132
96
  end
133
-
134
- @jar[cookie.domain][cookie.name] = cookie
135
- cleanup()
136
- cookie
137
- end
138
-
139
- # Fetch the cookies that should be used for the URI object passed in.
140
- def cookies(url)
141
- cleanup
142
- cookies = []
143
- @jar.each_key do |domain|
144
- if url.host =~ /#{domain}$/
145
- @jar[domain].each_key do |name|
146
- if url.path =~ /^#{@jar[domain][name].path}/
147
- if @jar[domain][name].expires.nil?
148
- cookies << @jar[domain][name]
149
- elsif DateTime.now < @jar[domain][name].expires
150
- cookies << @jar[domain][name]
97
+
98
+ # Add a cookie to the Jar.
99
+ def add(cookie)
100
+ unless @jar.has_key?(cookie.domain)
101
+ @jar[cookie.domain] = Hash.new
102
+ end
103
+
104
+ @jar[cookie.domain][cookie.name] = cookie
105
+ cleanup()
106
+ cookie
107
+ end
108
+
109
+ # Fetch the cookies that should be used for the URI object passed in.
110
+ def cookies(url)
111
+ cleanup
112
+ cookies = []
113
+ url.path = '/' if url.path.empty?
114
+ @jar.each_key do |domain|
115
+ if url.host =~ /#{domain}$/
116
+ @jar[domain].each_key do |name|
117
+ if url.path =~ /^#{@jar[domain][name].path}/
118
+ if @jar[domain][name].expires.nil?
119
+ cookies << @jar[domain][name]
120
+ elsif Time.now < @jar[domain][name].expires
121
+ cookies << @jar[domain][name]
122
+ end
151
123
  end
152
124
  end
153
125
  end
154
126
  end
127
+
128
+ cookies
129
+ end
130
+
131
+ def empty?(url)
132
+ cookies(url).length > 0 ? false : true
133
+ end
134
+
135
+ def to_a
136
+ cookies = []
137
+ @jar.each_key do |domain|
138
+ @jar[domain].each_key do |name|
139
+ cookies << @jar[domain][name]
140
+ end
141
+ end
142
+ cookies
143
+ end
144
+
145
+ # Save the cookie jar to a file as YAML
146
+ def save_as(file)
147
+ ::File.open(file, "w") { |f|
148
+ YAML::dump(@jar, f)
149
+ }
150
+ end
151
+
152
+ # Load cookie jar from a file stored as YAML
153
+ def load(file)
154
+ @jar = ::File.open(file) { |yf| YAML::load( yf ) }
155
155
  end
156
-
157
- cookies
158
- end
159
-
160
- def empty?(url)
161
- cookies(url).length > 0 ? false : true
162
- end
163
156
 
164
- private
157
+ # Clear the cookie jar
158
+ def clear!
159
+ @jar = {}
160
+ end
165
161
 
166
- # Remove expired cookies
167
- def cleanup
168
- @jar.each_key do |domain|
169
- @jar[domain].each_key do |name|
170
- if ! @jar[domain][name].expires.nil? &&
171
- DateTime.now > @jar[domain][name].expires
172
- @jar[domain].delete(name)
162
+ private
163
+ # Remove expired cookies
164
+ def cleanup
165
+ @jar.each_key do |domain|
166
+ @jar[domain].each_key do |name|
167
+ unless @jar[domain][name].expires.nil?
168
+ if Time.now > @jar[domain][name].expires
169
+ @jar[domain].delete(name)
170
+ end
171
+ end
173
172
  end
174
173
  end
175
174
  end
@@ -0,0 +1,16 @@
1
+ ***************
2
+ *** 140,145 ****
3
+ def cookies(url)
4
+ cleanup
5
+ cookies = []
6
+ @jar.each_key do |domain|
7
+ if url.host =~ /#{domain}$/
8
+ @jar[domain].each_key do |name|
9
+ --- 140,146 ----
10
+ def cookies(url)
11
+ cleanup
12
+ cookies = []
13
+ + url.path = '/' if url.path.empty?
14
+ @jar.each_key do |domain|
15
+ if url.host =~ /#{domain}$/
16
+ @jar[domain].each_key do |name|
@@ -0,0 +1,29 @@
1
+ module WWW
2
+ class Mechanize
3
+ # =Synopsis
4
+ # This class contains an error for when a pluggable parser tries to
5
+ # parse a content type that it does not know how to handle. For example
6
+ # if WWW::Mechanize::Page were to try to parse a PDF, a ContentTypeError
7
+ # would be thrown.
8
+ class ContentTypeError < RuntimeError
9
+ attr_reader :content_type
10
+
11
+ def initialize(content_type)
12
+ @content_type = content_type
13
+ end
14
+ end
15
+
16
+ # =Synopsis
17
+ # This error is thrown when Mechanize encounters a response code it does
18
+ # not know how to handle. Currently, this exception will be thrown
19
+ # if Mechanize encounters response codes other than 200, 301, or 302.
20
+ # Any other response code is up to the user to handle.
21
+ class ResponseCodeError < RuntimeError
22
+ attr_reader :response_code
23
+
24
+ def initialize(response_code)
25
+ @response_code = response_code
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,203 +1,228 @@
1
1
  module WWW
2
- # Class Form does not work in the case there is some invalid (unbalanced) html
3
- # involved, such as:
4
- #
5
- # <td>
6
- # <form>
7
- # </td>
8
- # <td>
9
- # <input .../>
10
- # </form>
11
- # </td>
12
- #
13
- # GlobalForm takes two nodes, the node where the form tag is located
14
- # (form_node), and another node, from which to start looking for form elements
15
- # (elements_node) like buttons and the like. For class Form both fall together
16
- # into one and the same node.
17
- class GlobalForm
18
- attr_reader :form_node, :elements_node
19
- attr_accessor :method, :action, :name
20
-
21
- attr_finder :fields, :buttons, :file_uploads, :radiobuttons, :checkboxes
22
- attr_reader :enctype
23
-
24
- def initialize(form_node, elements_node)
25
- @form_node, @elements_node = form_node, elements_node
26
-
27
- @method = (@form_node.attributes['method'] || 'GET').upcase
28
- @action = @form_node.attributes['action']
29
- @name = @form_node.attributes['name']
30
- @enctype = @form_node.attributes['enctype'] || 'application/x-www-form-urlencoded'
31
- @clicked_buttons = []
32
-
33
- parse
34
- end
35
-
36
- # In the case of malformed HTML, fields of multiple forms might occure in this forms'
37
- # field array. If the fields have the same name, posterior fields overwrite former fields.
38
- # To avoid this, this method rejects all posterior duplicate fields.
39
-
40
- def uniq_fields!
41
- names_in = {}
42
- fields.reject! {|f|
43
- if names_in.include?(f.name)
44
- true
45
- else
46
- names_in[f.name] = true
47
- false
48
- end
49
- }
50
- end
51
-
52
- def build_query(buttons = [])
53
- query = []
54
-
55
- fields().each do |f|
56
- next unless f.value
57
- query << [f.name, f.value]
2
+ class Mechanize
3
+ # =Synopsis
4
+ # GlobalForm provides all access to form fields, such as the buttons,
5
+ # check boxes, and text input.
6
+ #
7
+ # GlobalForm takes two nodes, the node where the form tag is located
8
+ # (form_node), and another node, from which to start looking for form
9
+ # elements (elements_node) like buttons and the like. For class Form
10
+ # both fall together into one and the same node.
11
+ #
12
+ # Class Form does not work in the case there is some invalid (unbalanced)
13
+ # html involved, such as:
14
+ #
15
+ # <td>
16
+ # <form>
17
+ # </td>
18
+ # <td>
19
+ # <input .../>
20
+ # </form>
21
+ # </td>
22
+ #
23
+ class GlobalForm
24
+ attr_reader :form_node, :elements_node
25
+ attr_accessor :method, :action, :name
26
+
27
+ attr_finder :fields, :buttons, :file_uploads, :radiobuttons, :checkboxes
28
+ attr_reader :enctype
29
+
30
+ def initialize(form_node, elements_node)
31
+ @form_node, @elements_node = form_node, elements_node
32
+
33
+ @method = (@form_node.attributes['method'] || 'GET').upcase
34
+ @action = @form_node.attributes['action']
35
+ @name = @form_node.attributes['name']
36
+ @enctype = @form_node.attributes['enctype'] || 'application/x-www-form-urlencoded'
37
+ @clicked_buttons = []
38
+
39
+ parse
58
40
  end
59
-
60
- checkboxes().each do |f|
61
- query << [f.name, f.value || "on"] if f.checked
41
+
42
+ # In the case of malformed HTML, fields of multiple forms might occure in this forms'
43
+ # field array. If the fields have the same name, posterior fields overwrite former fields.
44
+ # To avoid this, this method rejects all posterior duplicate fields.
45
+
46
+ def uniq_fields!
47
+ names_in = {}
48
+ fields.reject! {|f|
49
+ if names_in.include?(f.name)
50
+ true
51
+ else
52
+ names_in[f.name] = true
53
+ false
54
+ end
55
+ }
56
+ end
57
+
58
+ # This method builds an array of arrays that represent the query
59
+ # parameters to be used with this form. The return value can then
60
+ # be used to create a query string for this form.
61
+ def build_query(buttons = [])
62
+ query = []
63
+
64
+ fields().each do |f|
65
+ next unless f.value
66
+ query << [f.name, f.value]
67
+ end
68
+
69
+ checkboxes().each do |f|
70
+ query << [f.name, f.value || "on"] if f.checked
71
+ end
72
+
73
+ radio_groups = {}
74
+ radiobuttons().each do |f|
75
+ radio_groups[f.name] ||= []
76
+ radio_groups[f.name] << f
77
+ end
78
+
79
+ # take one radio button from each group
80
+ radio_groups.each_value do |g|
81
+ checked = g.select {|f| f.checked}
82
+
83
+ if checked.size == 1
84
+ f = checked.first
85
+ query << [f.name, f.value || ""]
86
+ elsif checked.size > 1
87
+ raise "multiple radiobuttons are checked in the same group!"
88
+ end
89
+ end
90
+
91
+ @clicked_buttons.each { |b|
92
+ b.add_to_query(query)
93
+ }
94
+
95
+ query
62
96
  end
63
-
64
- radio_groups = {}
65
- radiobuttons().each do |f|
66
- radio_groups[f.name] ||= []
67
- radio_groups[f.name] << f
97
+
98
+ # This method adds a button to the query. If the form needs to be
99
+ # submitted with multiple buttons, pass each button to this method.
100
+ def add_button_to_query(button)
101
+ @clicked_buttons << button
68
102
  end
69
-
70
- # take one radio button from each group
71
- radio_groups.each_value do |g|
72
- checked = g.select {|f| f.checked}
73
-
74
- if checked.size == 1
75
- f = checked.first
76
- query << [f.name, f.value || ""]
77
- elsif checked.size > 1
78
- raise "multiple radiobuttons are checked in the same group!"
103
+
104
+ # This method calculates the request data to be sent back to the server
105
+ # for this form, depending on if this is a regular post, get, or a
106
+ # multi-part post,
107
+ def request_data
108
+ query_params = build_query()
109
+ query = nil
110
+ case @enctype.downcase
111
+ when 'multipart/form-data'
112
+ boundary = rand_string(20)
113
+ @enctype << ", boundary=#{boundary}"
114
+ params = []
115
+ query_params.each { |k,v| params << param_to_multipart(k, v) }
116
+ @file_uploads.each { |f| params << file_to_multipart(f) }
117
+ query = params.collect { |p| "--#{boundary}\r\n#{p}" }.join('') +
118
+ "--#{boundary}--\r\n"
119
+ else
120
+ query = WWW::Mechanize.build_query_string(query_params)
79
121
  end
122
+
123
+ query
124
+ end
125
+
126
+ def inspect
127
+ "Form: ['#{@name}' #{@method} #{@action}]"
80
128
  end
81
129
 
82
- @clicked_buttons.each { |b|
83
- b.add_to_query(query)
84
- }
85
-
86
- query
87
- end
130
+ private
131
+ def parse
132
+ @fields = WWW::Mechanize::List.new
133
+ @buttons = WWW::Mechanize::List.new
134
+ @file_uploads = WWW::Mechanize::List.new
135
+ @radiobuttons = WWW::Mechanize::List.new
136
+ @checkboxes = WWW::Mechanize::List.new
137
+
138
+ @elements_node.each_recursive {|node|
139
+ case node.name.downcase
140
+ when 'input'
141
+ case (node.attributes['type'] || 'text').downcase
142
+ when 'text', 'password', 'hidden', 'int'
143
+ @fields << Field.new(node.attributes['name'], node.attributes['value'] || '')
144
+ when 'radio'
145
+ @radiobuttons << RadioButton.new(node.attributes['name'], node.attributes['value'], node.attributes.has_key?('checked'))
146
+ when 'checkbox'
147
+ @checkboxes << CheckBox.new(node.attributes['name'], node.attributes['value'], node.attributes.has_key?('checked'))
148
+ when 'file'
149
+ @file_uploads << FileUpload.new(node.attributes['name'], node.attributes['value'])
150
+ when 'submit'
151
+ @buttons << Button.new(node.attributes['name'], node.attributes['value'])
152
+ when 'image'
153
+ @buttons << ImageButton.new(node.attributes['name'], node.attributes['value'])
154
+ end
155
+ when 'textarea'
156
+ @fields << Field.new(node.attributes['name'], node.all_text)
157
+ when 'select'
158
+ @fields << SelectList.new(node.attributes['name'], node)
159
+ end
160
+ }
161
+ end
88
162
 
89
- def add_button_to_query(button)
90
- @clicked_buttons << button
91
- end
92
-
93
- def request_data
94
- query_params = build_query()
95
- query = nil
96
- case @enctype.downcase
97
- when 'multipart/form-data'
98
- boundary = rand_string(20)
99
- @enctype << ", boundary=#{boundary}"
100
- params = []
101
- query_params.each { |k,v| params << param_to_multipart(k, v) }
102
- @file_uploads.each { |f| params << file_to_multipart(f) }
103
- query = params.collect { |p| "--#{boundary}\r\n#{p}" }.join('') +
104
- "--#{boundary}--\r\n"
105
- else
106
- query = WWW::Mechanize.build_query_string(query_params)
163
+ def rand_string(len = 10)
164
+ chars = ("a".."z").to_a + ("A".."Z").to_a
165
+ string = ""
166
+ 1.upto(len) { |i| string << chars[rand(chars.size-1)] }
167
+ string
168
+ end
169
+
170
+ def mime_value_quote(str)
171
+ str.gsub(/(["\r\\])/){|s| '\\' + s}
107
172
  end
108
-
109
- query
110
- end
111
-
112
- def parse
113
- @fields = WWW::Mechanize::List.new
114
- @buttons = WWW::Mechanize::List.new
115
- @file_uploads = WWW::Mechanize::List.new
116
- @radiobuttons = WWW::Mechanize::List.new
117
- @checkboxes = WWW::Mechanize::List.new
118
-
119
- @elements_node.each_recursive {|node|
120
- case node.name.downcase
121
- when 'input'
122
- case (node.attributes['type'] || 'text').downcase
123
- when 'text', 'password', 'hidden', 'int'
124
- @fields << Field.new(node.attributes['name'], node.attributes['value'] || '')
125
- when 'radio'
126
- @radiobuttons << RadioButton.new(node.attributes['name'], node.attributes['value'], node.attributes.has_key?('checked'))
127
- when 'checkbox'
128
- @checkboxes << CheckBox.new(node.attributes['name'], node.attributes['value'], node.attributes.has_key?('checked'))
129
- when 'file'
130
- @file_uploads << FileUpload.new(node.attributes['name'], node.attributes['value'])
131
- when 'submit'
132
- @buttons << Button.new(node.attributes['name'], node.attributes['value'])
133
- when 'image'
134
- @buttons << ImageButton.new(node.attributes['name'], node.attributes['value'])
135
- end
136
- when 'textarea'
137
- @fields << Field.new(node.attributes['name'], node.all_text)
138
- when 'select'
139
- @fields << SelectList.new(node.attributes['name'], node)
140
- end
141
- }
142
- end
143
173
 
144
- def inspect
145
- string = "Form: ['#{@name}' -> #{@action}]\n"
146
- string << "[radiobuttons]\n"
147
- @radiobuttons.each { |f| string << f.inspect }
148
- string << "[checkboxes]\n"
149
- @checkboxes.each { |f| string << f.inspect }
150
- string << "[fields]\n"
151
- @fields.each { |f| string << f.inspect }
152
- string << "[buttons]\n"
153
- @buttons.each { |f| string << f.inspect }
154
- string
155
- end
156
-
157
- private
158
- def rand_string(len = 10)
159
- chars = ("a".."z").to_a + ("A".."Z").to_a
160
- string = ""
161
- 1.upto(len) { |i| string << chars[rand(chars.size-1)] }
162
- string
163
- end
164
-
165
- def mime_value_quote(str)
166
- str.gsub(/(["\r\\])/){|s| '\\' + s}
174
+ def param_to_multipart(name, value)
175
+ return "Content-Disposition: form-data; name=\"" +
176
+ "#{mime_value_quote(name)}\"\r\n" +
177
+ "\r\n#{value}\r\n"
178
+ end
179
+
180
+ def file_to_multipart(file)
181
+ body = "Content-Disposition: form-data; name=\"" +
182
+ "#{mime_value_quote(file.name)}\"; " +
183
+ "filename=\"#{mime_value_quote(file.file_name)}\"\r\n" +
184
+ "Content-Transfer-Encoding: binary\r\n"
185
+ if file.mime_type != nil
186
+ body << "Content-Type: #{file.mime_type}\r\n"
187
+ end
188
+
189
+ body << "\r\n#{file.file_data}\r\n"
190
+
191
+ body
192
+ end
167
193
  end
194
+
195
+ # =Synopsis
196
+ # This class encapsulates a form parsed out of an HTML page. Each type
197
+ # of input fields available in a form can be accessed through this object.
198
+ # See GlobalForm for more methods.
199
+ #
200
+ # ==Example
201
+ # Find a form and print out its fields
202
+ # form = page.forms.first # => WWW::Mechanize::Form
203
+ # form.fields.each { |f| puts f.name }
204
+ class Form < GlobalForm
205
+ attr_reader :node
206
+
207
+ def initialize(node)
208
+ @node = node
209
+ super(@node, @node)
210
+ end
168
211
 
169
- def param_to_multipart(name, value)
170
- return "Content-Disposition: form-data; name=\"" +
171
- "#{mime_value_quote(name)}\"\r\n" +
172
- "\r\n#{value}\r\n"
173
- end
174
-
175
- def file_to_multipart(file)
176
- body = "Content-Disposition: form-data; name=\"" +
177
- "#{mime_value_quote(file.name)}\"; " +
178
- "filename=\"#{mime_value_quote(file.file_name)}\"\r\n" +
179
- "Content-Transfer-Encoding: binary\r\n"
180
- if file.mime_type != nil
181
- body << "Content-Type: #{file.mime_type}\r\n"
212
+ # Fetch the first field whose name is equal to field_name
213
+ def field(field_name)
214
+ fields.find { |f| f.name.eql? field_name }
182
215
  end
183
-
184
- body << "\r\n#{file.file_data}\r\n"
185
-
186
- body
187
- end
188
- end
189
-
190
- class Form < GlobalForm
191
- attr_reader :node
192
-
193
- def initialize(node)
194
- @node = node
195
- super(@node, @node)
196
- end
197
216
 
198
- # Fetch the first field whose name is equal to field_name
199
- def field(field_name)
200
- fields.find { |f| f.name.eql? field_name }
217
+ # Treat form fields like accessors.
218
+ def method_missing(id,*args)
219
+ method = id.to_s.gsub(/=$/, '')
220
+ if field(method)
221
+ return field(method).value if args.empty?
222
+ return field(method).value = args[0]
223
+ end
224
+ super
225
+ end
201
226
  end
202
227
  end
203
228
  end