lack 2.0.0
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.
- checksums.yaml +7 -0
- data/bin/rackup +5 -0
- data/lib/rack.rb +26 -0
- data/lib/rack/body_proxy.rb +39 -0
- data/lib/rack/builder.rb +166 -0
- data/lib/rack/handler.rb +63 -0
- data/lib/rack/handler/webrick.rb +120 -0
- data/lib/rack/mime.rb +661 -0
- data/lib/rack/mock.rb +198 -0
- data/lib/rack/multipart.rb +31 -0
- data/lib/rack/multipart/generator.rb +93 -0
- data/lib/rack/multipart/parser.rb +239 -0
- data/lib/rack/multipart/uploaded_file.rb +34 -0
- data/lib/rack/request.rb +394 -0
- data/lib/rack/response.rb +160 -0
- data/lib/rack/server.rb +258 -0
- data/lib/rack/server/options.rb +121 -0
- data/lib/rack/utils.rb +653 -0
- data/lib/rack/version.rb +3 -0
- data/spec/spec_helper.rb +1 -0
- data/test/builder/anything.rb +5 -0
- data/test/builder/comment.ru +4 -0
- data/test/builder/end.ru +5 -0
- data/test/builder/line.ru +1 -0
- data/test/builder/options.ru +2 -0
- data/test/multipart/bad_robots +259 -0
- data/test/multipart/binary +0 -0
- data/test/multipart/content_type_and_no_filename +6 -0
- data/test/multipart/empty +10 -0
- data/test/multipart/fail_16384_nofile +814 -0
- data/test/multipart/file1.txt +1 -0
- data/test/multipart/filename_and_modification_param +7 -0
- data/test/multipart/filename_and_no_name +6 -0
- data/test/multipart/filename_with_escaped_quotes +6 -0
- data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
- data/test/multipart/filename_with_percent_escaped_quotes +6 -0
- data/test/multipart/filename_with_unescaped_percentages +6 -0
- data/test/multipart/filename_with_unescaped_percentages2 +6 -0
- data/test/multipart/filename_with_unescaped_percentages3 +6 -0
- data/test/multipart/filename_with_unescaped_quotes +6 -0
- data/test/multipart/ie +6 -0
- data/test/multipart/invalid_character +6 -0
- data/test/multipart/mixed_files +21 -0
- data/test/multipart/nested +10 -0
- data/test/multipart/none +9 -0
- data/test/multipart/semicolon +6 -0
- data/test/multipart/text +15 -0
- data/test/multipart/webkit +32 -0
- data/test/rackup/config.ru +31 -0
- data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
- data/test/spec_body_proxy.rb +69 -0
- data/test/spec_builder.rb +223 -0
- data/test/spec_chunked.rb +101 -0
- data/test/spec_file.rb +221 -0
- data/test/spec_handler.rb +59 -0
- data/test/spec_head.rb +45 -0
- data/test/spec_lint.rb +522 -0
- data/test/spec_mime.rb +51 -0
- data/test/spec_mock.rb +277 -0
- data/test/spec_multipart.rb +547 -0
- data/test/spec_recursive.rb +72 -0
- data/test/spec_request.rb +1199 -0
- data/test/spec_response.rb +343 -0
- data/test/spec_rewindable_input.rb +118 -0
- data/test/spec_sendfile.rb +130 -0
- data/test/spec_server.rb +167 -0
- data/test/spec_utils.rb +635 -0
- data/test/spec_webrick.rb +184 -0
- data/test/testrequest.rb +78 -0
- data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
- data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
- metadata +240 -0
data/lib/rack/mock.rb
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'stringio'
|
3
|
+
require 'rack'
|
4
|
+
require 'rack/lint'
|
5
|
+
require 'rack/utils'
|
6
|
+
require 'rack/response'
|
7
|
+
|
8
|
+
module Rack
|
9
|
+
# Rack::MockRequest helps testing your Rack application without
|
10
|
+
# actually using HTTP.
|
11
|
+
#
|
12
|
+
# After performing a request on a URL with get/post/put/patch/delete, it
|
13
|
+
# returns a MockResponse with useful helper methods for effective
|
14
|
+
# testing.
|
15
|
+
#
|
16
|
+
# You can pass a hash with additional configuration to the
|
17
|
+
# get/post/put/patch/delete.
|
18
|
+
# <tt>:input</tt>:: A String or IO-like to be used as rack.input.
|
19
|
+
# <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
|
20
|
+
# <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
|
21
|
+
|
22
|
+
class MockRequest
|
23
|
+
class FatalWarning < RuntimeError
|
24
|
+
end
|
25
|
+
|
26
|
+
class FatalWarner
|
27
|
+
def puts(warning)
|
28
|
+
raise FatalWarning, warning
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(warning)
|
32
|
+
raise FatalWarning, warning
|
33
|
+
end
|
34
|
+
|
35
|
+
def flush
|
36
|
+
end
|
37
|
+
|
38
|
+
def string
|
39
|
+
""
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
DEFAULT_ENV = {
|
44
|
+
"rack.version" => Rack::VERSION,
|
45
|
+
"rack.input" => StringIO.new,
|
46
|
+
"rack.errors" => StringIO.new,
|
47
|
+
"rack.multithread" => true,
|
48
|
+
"rack.multiprocess" => true,
|
49
|
+
"rack.run_once" => false,
|
50
|
+
}
|
51
|
+
|
52
|
+
def initialize(app)
|
53
|
+
@app = app
|
54
|
+
end
|
55
|
+
|
56
|
+
def get(uri, opts={}) request("GET", uri, opts) end
|
57
|
+
def post(uri, opts={}) request("POST", uri, opts) end
|
58
|
+
def put(uri, opts={}) request("PUT", uri, opts) end
|
59
|
+
def patch(uri, opts={}) request("PATCH", uri, opts) end
|
60
|
+
def delete(uri, opts={}) request("DELETE", uri, opts) end
|
61
|
+
def head(uri, opts={}) request("HEAD", uri, opts) end
|
62
|
+
def options(uri, opts={}) request("OPTIONS", uri, opts) end
|
63
|
+
|
64
|
+
def request(method="GET", uri="", opts={})
|
65
|
+
env = self.class.env_for(uri, opts.merge(:method => method))
|
66
|
+
|
67
|
+
if opts[:lint]
|
68
|
+
app = Rack::Lint.new(@app)
|
69
|
+
else
|
70
|
+
app = @app
|
71
|
+
end
|
72
|
+
|
73
|
+
errors = env["rack.errors"]
|
74
|
+
status, headers, body = app.call(env)
|
75
|
+
MockResponse.new(status, headers, body, errors)
|
76
|
+
ensure
|
77
|
+
body.close if body.respond_to?(:close)
|
78
|
+
end
|
79
|
+
|
80
|
+
# For historical reasons, we're pinning to RFC 2396. It's easier for users
|
81
|
+
# and we get support from ruby 1.8 to 2.2 using this method.
|
82
|
+
def self.parse_uri_rfc2396(uri)
|
83
|
+
@parser ||= defined?(URI::RFC2396_Parser) ? URI::RFC2396_Parser.new : URI
|
84
|
+
@parser.parse(uri)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Return the Rack environment used for a request to +uri+.
|
88
|
+
def self.env_for(uri="", opts={})
|
89
|
+
uri = parse_uri_rfc2396(uri)
|
90
|
+
uri.path = "/#{uri.path}" unless uri.path[0] == ?/
|
91
|
+
|
92
|
+
env = DEFAULT_ENV.dup
|
93
|
+
|
94
|
+
env["REQUEST_METHOD"] = opts[:method] ? opts[:method].to_s.upcase : "GET"
|
95
|
+
env["SERVER_NAME"] = uri.host || "example.org"
|
96
|
+
env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
|
97
|
+
env["QUERY_STRING"] = uri.query.to_s
|
98
|
+
env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path
|
99
|
+
env["rack.url_scheme"] = uri.scheme || "http"
|
100
|
+
env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off"
|
101
|
+
|
102
|
+
env["SCRIPT_NAME"] = opts[:script_name] || ""
|
103
|
+
|
104
|
+
if opts[:fatal]
|
105
|
+
env["rack.errors"] = FatalWarner.new
|
106
|
+
else
|
107
|
+
env["rack.errors"] = StringIO.new
|
108
|
+
end
|
109
|
+
|
110
|
+
if params = opts[:params]
|
111
|
+
if env["REQUEST_METHOD"] == "GET"
|
112
|
+
params = Utils.parse_nested_query(params) if params.is_a?(String)
|
113
|
+
params.update(Utils.parse_nested_query(env["QUERY_STRING"]))
|
114
|
+
env["QUERY_STRING"] = Utils.build_nested_query(params)
|
115
|
+
elsif !opts.has_key?(:input)
|
116
|
+
opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
|
117
|
+
if params.is_a?(Hash)
|
118
|
+
if data = Utils::Multipart.build_multipart(params)
|
119
|
+
opts[:input] = data
|
120
|
+
opts["CONTENT_LENGTH"] ||= data.length.to_s
|
121
|
+
opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Utils::Multipart::MULTIPART_BOUNDARY}"
|
122
|
+
else
|
123
|
+
opts[:input] = Utils.build_nested_query(params)
|
124
|
+
end
|
125
|
+
else
|
126
|
+
opts[:input] = params
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
empty_str = ""
|
132
|
+
empty_str.force_encoding("ASCII-8BIT") if empty_str.respond_to? :force_encoding
|
133
|
+
opts[:input] ||= empty_str
|
134
|
+
if String === opts[:input]
|
135
|
+
rack_input = StringIO.new(opts[:input])
|
136
|
+
else
|
137
|
+
rack_input = opts[:input]
|
138
|
+
end
|
139
|
+
|
140
|
+
rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
|
141
|
+
env['rack.input'] = rack_input
|
142
|
+
|
143
|
+
env["CONTENT_LENGTH"] ||= env["rack.input"].length.to_s
|
144
|
+
|
145
|
+
opts.each { |field, value|
|
146
|
+
env[field] = value if String === field
|
147
|
+
}
|
148
|
+
|
149
|
+
env
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Rack::MockResponse provides useful helpers for testing your apps.
|
154
|
+
# Usually, you don't create the MockResponse on your own, but use
|
155
|
+
# MockRequest.
|
156
|
+
|
157
|
+
class MockResponse < Rack::Response
|
158
|
+
# Headers
|
159
|
+
attr_reader :original_headers
|
160
|
+
|
161
|
+
# Errors
|
162
|
+
attr_accessor :errors
|
163
|
+
|
164
|
+
def initialize(status, headers, body, errors=StringIO.new(""))
|
165
|
+
@original_headers = headers
|
166
|
+
@errors = errors.string if errors.respond_to?(:string)
|
167
|
+
@body_string = nil
|
168
|
+
|
169
|
+
super(body, status, headers)
|
170
|
+
end
|
171
|
+
|
172
|
+
def =~(other)
|
173
|
+
body =~ other
|
174
|
+
end
|
175
|
+
|
176
|
+
def match(other)
|
177
|
+
body.match other
|
178
|
+
end
|
179
|
+
|
180
|
+
def body
|
181
|
+
# FIXME: apparently users of MockResponse expect the return value of
|
182
|
+
# MockResponse#body to be a string. However, the real response object
|
183
|
+
# returns the body as a list.
|
184
|
+
#
|
185
|
+
# See spec_showstatus.rb:
|
186
|
+
#
|
187
|
+
# should "not replace existing messages" do
|
188
|
+
# ...
|
189
|
+
# res.body.should == "foo!"
|
190
|
+
# end
|
191
|
+
super.join
|
192
|
+
end
|
193
|
+
|
194
|
+
def empty?
|
195
|
+
[201, 204, 205, 304].include? status
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Rack
|
2
|
+
# A multipart form data parser, adapted from IOWA.
|
3
|
+
#
|
4
|
+
# Usually, Rack::Request#POST takes care of calling this.
|
5
|
+
module Multipart
|
6
|
+
require_relative "multipart/uploaded_file"
|
7
|
+
require_relative "multipart/parser"
|
8
|
+
require_relative "multipart/generator"
|
9
|
+
|
10
|
+
EOL = "\r\n"
|
11
|
+
MULTIPART_BOUNDARY = "AaB03x"
|
12
|
+
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
|
13
|
+
TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
|
14
|
+
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
|
15
|
+
DISPPARM = /;\s*(#{TOKEN})=("(?:\\"|[^"])*"|#{TOKEN})/
|
16
|
+
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
|
17
|
+
BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
|
18
|
+
BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i
|
19
|
+
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
|
20
|
+
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name="?([^\";]*)"?/ni
|
21
|
+
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
|
22
|
+
|
23
|
+
def self.parse_multipart(env)
|
24
|
+
Parser.create(env).parse
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.build_multipart(params, first = true)
|
28
|
+
Generator.new(params, first).dump
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Rack
|
2
|
+
module Multipart
|
3
|
+
class Generator
|
4
|
+
def initialize(params, first = true)
|
5
|
+
@params, @first = params, first
|
6
|
+
|
7
|
+
if @first && !@params.is_a?(Hash)
|
8
|
+
raise ArgumentError, "value must be a Hash"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def dump
|
13
|
+
return nil if @first && !multipart?
|
14
|
+
return flattened_params if !@first
|
15
|
+
|
16
|
+
flattened_params.map do |name, file|
|
17
|
+
if file.respond_to?(:original_filename)
|
18
|
+
::File.open(file.path, "rb") do |f|
|
19
|
+
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
|
20
|
+
content_for_tempfile(f, file, name)
|
21
|
+
end
|
22
|
+
else
|
23
|
+
content_for_other(file, name)
|
24
|
+
end
|
25
|
+
end.join + "--#{MULTIPART_BOUNDARY}--\r"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def multipart?
|
30
|
+
multipart = false
|
31
|
+
|
32
|
+
query = lambda { |value|
|
33
|
+
case value
|
34
|
+
when Array
|
35
|
+
value.each(&query)
|
36
|
+
when Hash
|
37
|
+
value.values.each(&query)
|
38
|
+
when Rack::Multipart::UploadedFile
|
39
|
+
multipart = true
|
40
|
+
end
|
41
|
+
}
|
42
|
+
@params.values.each(&query)
|
43
|
+
|
44
|
+
multipart
|
45
|
+
end
|
46
|
+
|
47
|
+
def flattened_params
|
48
|
+
@flattened_params ||= begin
|
49
|
+
h = Hash.new
|
50
|
+
@params.each do |key, value|
|
51
|
+
k = @first ? key.to_s : "[#{key}]"
|
52
|
+
|
53
|
+
case value
|
54
|
+
when Array
|
55
|
+
value.map { |v|
|
56
|
+
Multipart.build_multipart(v, false).each { |subkey, subvalue|
|
57
|
+
h["#{k}[]#{subkey}"] = subvalue
|
58
|
+
}
|
59
|
+
}
|
60
|
+
when Hash
|
61
|
+
Multipart.build_multipart(value, false).each { |subkey, subvalue|
|
62
|
+
h[k + subkey] = subvalue
|
63
|
+
}
|
64
|
+
else
|
65
|
+
h[k] = value
|
66
|
+
end
|
67
|
+
end
|
68
|
+
h
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def content_for_tempfile(io, file, name)
|
73
|
+
<<-EOF
|
74
|
+
--#{MULTIPART_BOUNDARY}\r
|
75
|
+
Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
|
76
|
+
Content-Type: #{file.content_type}\r
|
77
|
+
Content-Length: #{::File.stat(file.path).size}\r
|
78
|
+
\r
|
79
|
+
#{io.read}\r
|
80
|
+
EOF
|
81
|
+
end
|
82
|
+
|
83
|
+
def content_for_other(file, name)
|
84
|
+
<<-EOF
|
85
|
+
--#{MULTIPART_BOUNDARY}\r
|
86
|
+
Content-Disposition: form-data; name="#{name}"\r
|
87
|
+
\r
|
88
|
+
#{file}\r
|
89
|
+
EOF
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
|
2
|
+
module Rack
|
3
|
+
module Multipart
|
4
|
+
class Parser
|
5
|
+
BUFSIZE = 16384
|
6
|
+
|
7
|
+
DUMMY = Struct.new(:parse).new
|
8
|
+
|
9
|
+
def self.create(env)
|
10
|
+
return DUMMY unless env["CONTENT_TYPE"] =~ MULTIPART
|
11
|
+
|
12
|
+
io = env["rack.input"]
|
13
|
+
io.rewind
|
14
|
+
|
15
|
+
content_length = env["CONTENT_LENGTH"]
|
16
|
+
content_length = content_length.to_i if content_length
|
17
|
+
|
18
|
+
new($1, io, content_length, env)
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(boundary, io, content_length, env)
|
22
|
+
@buf = ""
|
23
|
+
|
24
|
+
if @buf.respond_to? :force_encoding
|
25
|
+
@buf.force_encoding Encoding::ASCII_8BIT
|
26
|
+
end
|
27
|
+
|
28
|
+
@params = Utils::KeySpaceConstrainedParams.new
|
29
|
+
@boundary = "--#{boundary}"
|
30
|
+
@io = io
|
31
|
+
@content_length = content_length
|
32
|
+
@boundary_size = Utils.bytesize(@boundary) + EOL.size
|
33
|
+
@env = env
|
34
|
+
|
35
|
+
if @content_length
|
36
|
+
@content_length -= @boundary_size
|
37
|
+
end
|
38
|
+
|
39
|
+
@rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
|
40
|
+
@full_boundary = @boundary + EOL
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse
|
44
|
+
fast_forward_to_first_boundary
|
45
|
+
|
46
|
+
loop do
|
47
|
+
head, filename, content_type, name, body =
|
48
|
+
get_current_head_and_filename_and_content_type_and_name_and_body
|
49
|
+
|
50
|
+
# Save the rest.
|
51
|
+
if i = @buf.index(rx)
|
52
|
+
body << @buf.slice!(0, i)
|
53
|
+
@buf.slice!(0, @boundary_size+2)
|
54
|
+
|
55
|
+
@content_length = -1 if $1 == "--"
|
56
|
+
end
|
57
|
+
|
58
|
+
get_data(filename, body, content_type, name, head) do |data|
|
59
|
+
tag_multipart_encoding(filename, content_type, name, data)
|
60
|
+
|
61
|
+
Utils.normalize_params(@params, name, data)
|
62
|
+
end
|
63
|
+
|
64
|
+
# break if we're at the end of a buffer, but not if it is the end of a field
|
65
|
+
break if (@buf.empty? && $1 != EOL) || @content_length == -1
|
66
|
+
end
|
67
|
+
|
68
|
+
@io.rewind
|
69
|
+
|
70
|
+
@params.to_params_hash
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
def full_boundary; @full_boundary; end
|
75
|
+
|
76
|
+
def rx; @rx; end
|
77
|
+
|
78
|
+
def fast_forward_to_first_boundary
|
79
|
+
loop do
|
80
|
+
content = @io.read(BUFSIZE)
|
81
|
+
raise EOFError, "bad content body" unless content
|
82
|
+
@buf << content
|
83
|
+
|
84
|
+
while @buf.gsub!(/\A([^\n]*\n)/, "")
|
85
|
+
read_buffer = $1
|
86
|
+
return if read_buffer == full_boundary
|
87
|
+
end
|
88
|
+
|
89
|
+
raise EOFError, "bad content body" if Utils.bytesize(@buf) >= BUFSIZE
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def get_current_head_and_filename_and_content_type_and_name_and_body
|
94
|
+
head = nil
|
95
|
+
body = ""
|
96
|
+
|
97
|
+
if body.respond_to? :force_encoding
|
98
|
+
body.force_encoding Encoding::ASCII_8BIT
|
99
|
+
end
|
100
|
+
|
101
|
+
filename = content_type = name = nil
|
102
|
+
|
103
|
+
until head && @buf =~ rx
|
104
|
+
if !head && i = @buf.index(EOL+EOL)
|
105
|
+
head = @buf.slice!(0, i+2) # First \r\n
|
106
|
+
|
107
|
+
@buf.slice!(0, 2) # Second \r\n
|
108
|
+
|
109
|
+
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
110
|
+
name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]
|
111
|
+
|
112
|
+
filename = get_filename(head)
|
113
|
+
|
114
|
+
if name.nil? || name.empty? && filename
|
115
|
+
name = filename
|
116
|
+
end
|
117
|
+
|
118
|
+
if filename
|
119
|
+
extname = ::File.extname(filename)
|
120
|
+
(@env["rack.tempfiles"] ||= []) << body = Tempfile.new(["RackMultipart", extname])
|
121
|
+
body.binmode if body.respond_to?(:binmode)
|
122
|
+
end
|
123
|
+
|
124
|
+
next
|
125
|
+
end
|
126
|
+
|
127
|
+
# Save the read body part.
|
128
|
+
if head && (@boundary_size+4 < @buf.size)
|
129
|
+
body << @buf.slice!(0, @buf.size - (@boundary_size+4))
|
130
|
+
end
|
131
|
+
|
132
|
+
content = @io.read(@content_length && BUFSIZE >= @content_length ? @content_length : BUFSIZE)
|
133
|
+
raise EOFError, "bad content body" if content.nil? || content.empty?
|
134
|
+
|
135
|
+
@buf << content
|
136
|
+
@content_length -= content.size if @content_length
|
137
|
+
end
|
138
|
+
|
139
|
+
[head, filename, content_type, name, body]
|
140
|
+
end
|
141
|
+
|
142
|
+
def get_filename(head)
|
143
|
+
filename = nil
|
144
|
+
case head
|
145
|
+
when RFC2183
|
146
|
+
filename = Hash[head.scan(DISPPARM)]["filename"]
|
147
|
+
filename = $1 if filename and filename =~ /^"(.*)"$/
|
148
|
+
when BROKEN_QUOTED, BROKEN_UNQUOTED
|
149
|
+
filename = $1
|
150
|
+
end
|
151
|
+
|
152
|
+
return unless filename
|
153
|
+
|
154
|
+
if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
|
155
|
+
filename = Utils.unescape(filename)
|
156
|
+
end
|
157
|
+
|
158
|
+
scrub_filename filename
|
159
|
+
|
160
|
+
if filename !~ /\\[^\\"]/
|
161
|
+
filename = filename.gsub(/\\(.)/, '\1')
|
162
|
+
end
|
163
|
+
filename
|
164
|
+
end
|
165
|
+
|
166
|
+
if "<3".respond_to? :valid_encoding?
|
167
|
+
def scrub_filename(filename)
|
168
|
+
unless filename.valid_encoding?
|
169
|
+
# FIXME: this force_encoding is for Ruby 2.0 and 1.9 support.
|
170
|
+
# We can remove it after they are dropped
|
171
|
+
filename.force_encoding(Encoding::ASCII_8BIT)
|
172
|
+
filename.encode!(:invalid => :replace, :undef => :replace)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
CHARSET = "charset"
|
177
|
+
TEXT_PLAIN = "text/plain"
|
178
|
+
|
179
|
+
def tag_multipart_encoding(filename, content_type, name, body)
|
180
|
+
name.force_encoding Encoding::UTF_8
|
181
|
+
|
182
|
+
return if filename
|
183
|
+
|
184
|
+
encoding = Encoding::UTF_8
|
185
|
+
|
186
|
+
if content_type
|
187
|
+
list = content_type.split(";")
|
188
|
+
type_subtype = list.first
|
189
|
+
type_subtype.strip!
|
190
|
+
if TEXT_PLAIN == type_subtype
|
191
|
+
rest = list.drop 1
|
192
|
+
rest.each do |param|
|
193
|
+
k,v = param.split("=", 2)
|
194
|
+
k.strip!
|
195
|
+
v.strip!
|
196
|
+
encoding = Encoding.find v if k == CHARSET
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
name.force_encoding encoding
|
202
|
+
body.force_encoding encoding
|
203
|
+
end
|
204
|
+
else
|
205
|
+
def scrub_filename(filename)
|
206
|
+
end
|
207
|
+
def tag_multipart_encoding(filename, content_type, name, body)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def get_data(filename, body, content_type, name, head)
|
212
|
+
data = body
|
213
|
+
if filename == ""
|
214
|
+
# filename is blank which means no file has been selected
|
215
|
+
return
|
216
|
+
elsif filename
|
217
|
+
body.rewind
|
218
|
+
|
219
|
+
# Take the basename of the upload's original filename.
|
220
|
+
# This handles the full Windows paths given by Internet Explorer
|
221
|
+
# (and perhaps other broken user agents) without affecting
|
222
|
+
# those which give the lone filename.
|
223
|
+
filename = filename.split(/[\/\\]/).last
|
224
|
+
|
225
|
+
data = {:filename => filename, :type => content_type,
|
226
|
+
:name => name, :tempfile => body, :head => head}
|
227
|
+
elsif !filename && content_type && body.is_a?(IO)
|
228
|
+
body.rewind
|
229
|
+
|
230
|
+
# Generic multipart cases, not coming from a form
|
231
|
+
data = {:type => content_type,
|
232
|
+
:name => name, :tempfile => body, :head => head}
|
233
|
+
end
|
234
|
+
|
235
|
+
yield data
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|