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.
- data/CHANGELOG +17 -0
- data/EXAMPLES +23 -44
- data/NOTES +49 -0
- data/lib/mechanize.rb +95 -80
- data/lib/mechanize/cookie.rb +147 -148
- data/lib/mechanize/cookie.rb.rej +16 -0
- data/lib/mechanize/errors.rb +29 -0
- data/lib/mechanize/form.rb +211 -186
- data/lib/mechanize/form_elements.rb +31 -71
- data/lib/mechanize/list.rb +34 -0
- data/lib/mechanize/mech_version.rb +3 -1
- data/lib/mechanize/module.rb +1 -1
- data/lib/mechanize/page.rb +162 -180
- data/lib/mechanize/page_elements.rb +53 -40
- data/lib/mechanize/parsing.rb +11 -3
- data/lib/mechanize/pluggable_parsers.rb +147 -0
- data/test/data/server.crt +14 -0
- data/test/data/server.csr +11 -0
- data/test/data/server.key +18 -0
- data/test/data/server.pem +15 -0
- data/test/htdocs/no_title_test.html +6 -0
- data/test/parse.rb +39 -0
- data/test/proxy.rb +30 -0
- data/test/server.rb +2 -0
- data/test/servlets.rb +8 -0
- data/test/ssl_server.rb +49 -0
- data/test/tc_authenticate.rb +8 -6
- data/test/tc_cookie_class.rb +28 -18
- data/test/tc_cookie_jar.rb +88 -27
- data/test/tc_cookies.rb +41 -44
- data/test/tc_errors.rb +9 -23
- data/test/tc_forms.rb +36 -32
- data/test/tc_frames.rb +6 -4
- data/test/tc_links.rb +7 -6
- data/test/tc_mech.rb +43 -46
- data/test/tc_page.rb +24 -0
- data/test/tc_pluggable_parser.rb +103 -0
- data/test/tc_post_form.rb +41 -0
- data/test/tc_proxy.rb +25 -0
- data/test/tc_response_code.rb +13 -10
- data/test/tc_save_file.rb +25 -0
- data/test/tc_ssl_server.rb +27 -0
- data/test/tc_upload.rb +8 -6
- data/test/tc_watches.rb +5 -2
- data/test/test_includes.rb +3 -3
- data/test/ts_mech.rb +11 -2
- metadata +100 -86
- data/test/tc_filter.rb +0 -34
data/lib/mechanize/cookie.rb
CHANGED
@@ -1,175 +1,174 @@
|
|
1
|
-
require '
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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 =
|
49
|
-
rescue
|
50
|
-
|
51
|
-
break
|
40
|
+
time = Time::parse(expires_val)
|
41
|
+
rescue
|
42
|
+
time = Time.now
|
52
43
|
end
|
53
|
-
}
|
54
44
|
|
55
|
-
|
56
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
cookie
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
157
|
+
# Clear the cookie jar
|
158
|
+
def clear!
|
159
|
+
@jar = {}
|
160
|
+
end
|
165
161
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
@jar
|
170
|
-
|
171
|
-
|
172
|
-
@jar[domain].
|
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
|
data/lib/mechanize/form.rb
CHANGED
@@ -1,203 +1,228 @@
|
|
1
1
|
module WWW
|
2
|
-
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
61
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
#
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
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
|
-
|
199
|
-
|
200
|
-
|
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
|