opal/browser/http/request.rb
# backtick_javascript: true
module Browser; module HTTP
class Request
include Native::Wrapper
include Event::Target
# Default headers.
HEADERS = {
'X-Requested-With' => 'XMLHttpRequest',
'X-Opal-Version' => RUBY_ENGINE_VERSION,
'Accept' => 'text/javascript, text/html, application/xml, text/xml, */*'
}
STATES = %w[uninitialized loading loaded interactive complete]
# @!attribute [r] headers
# @return [Headers] the request headers
attr_reader :headers
# @!attribute [r] response
# @return [Response] the response associated with this request
attr_reader :response
# @!attribute [r] method
# @return [Symbol] the HTTP method for this request
attr_reader :method
# @!attribute [r] url
# @return [String, #to_s] the URL for this request
attr_reader :url
# Create a request with the optionally given configuration block.
#
# @yield [request] if the block has a parameter the request is passed
# otherwise it's instance_exec'd
def initialize(&block)
super(transport)
@parameters = {}
@query = {}
@headers = Headers[HEADERS]
@method = :get
@asynchronous = true
@binary = false
@cacheable = true
@opened = false
@sent = false
@completed = false
@callbacks = Hash.new { |h, k| h[k] = [] }
if block.arity == 0
instance_exec(&block)
else
block.call(self)
end if block
end
# @!method transport
# @private
if Browser.supports? :XHR
def transport
`new XMLHttpRequest()`
end
elsif Browser.supports? :ActiveX
def transport
`new ActiveXObject("MSXML2.XMLHTTP.3.0")`
end
else
def transport
raise NotImplementedError
end
end
# Check if the request has been opened.
def opened?
@opened
end
# Check if the request has been sent.
def sent?
@sent
end
# Check if the request has completed.
def completed?
@completed
end
# Check the request is asynchronous.
def asynchronous?
@asynchronous
end
# Check the request is synchronous.
def synchronous?
!@asynchronous
end
# Make the request asynchronous.
def asynchronous!
@asynchronous = true
end
# Make the request synchronous.
def synchronous!
@asynchronous = false
end
# Check the request is binary.
def binary?
@binary
end
# Make the request binary.
def binary!
@binary = true
end
# Check if the request is cacheable.
def cacheable?
@cacheable
end
# Disable caching for this request.
def no_cache!
@cacheable = false
end
# Get or set the user used for authentication.
#
# @param value [String] when passed it sets, when omitted it gets
#
# @return [String]
def user(value = nil)
value ? @user = value : @user
end
# Get or set the password used for authentication.
#
# @param value [String] when passed it sets, when omitted it gets
#
# @return [String]
def password(value = nil)
value ? @password = value : @password
end
# Get or set the MIME type of the request.
#
# @param value [String] when passed it sets, when omitted it gets
#
# @return [String]
def mime_type(value = nil)
value ? @mime_type = value : @mime_type
end
# Get or set the Content-Type of the request.
#
# @param value [String] when passed it sets, when omitted it gets
#
# @return [String]
def content_type(value = nil)
value ? @content_type = value : @content_type
end
# Get or set the encoding of the request.
#
# @param value [String] when passed it sets, when omitted it gets
#
# @return [String]
def encoding(value = nil)
value ? @encoding = value : @encoding
end
# Set the request parameters.
#
# @param hash [Hash] the parameters
#
# @return [Hash]
def parameters(hash = nil)
hash ? @parameters = hash : @parameters
end
# Set the URI query.
#
# @param hash [Hash] the query
#
# @return [Hash]
def query(hash = nil)
hash ? @query = hash : @query
end
# Register an event on the request.
#
# @param what [Symbol, String] the event name
#
# @yieldparam response [Response] the response for the event
def on(what, *, &block)
if STATES.include?(what) || %w[success failure].include?(what) || Integer === what
@callbacks[what] << block
else
super
end
end
# Open the request.
#
# @param method [Symbol] the HTTP method to use
# @param url [String, #to_s] the URL to send the request to
# @param asynchronous [Boolean] whether the request is asynchronous or not
# @param user [String] the user to use for authentication
# @param password [String] the password to use for authentication
#
# @return [self]
def open(method = nil, url = nil, asynchronous = nil, user = nil, password = nil)
raise 'the request has already been opened' if opened?
@method = method unless method.nil?
@url = url unless url.nil?
@asynchronous = asynchronous unless asynchronous.nil?
@user = user unless user.nil?
@password = password unless password.nil?
url = @url
# add a dummy random parameter to the query to try circumvent caching
unless cacheable?
@query[:_] = rand
end
# add the encoded query to the @url, prepending the right character if
# there was already a query in the defined @url or not
unless @query.empty?
if url.include? ??
url += ?&
else
url += ??
end
url += FormData.build_query(@query)
end
`#@native.open(#{@method.to_s.upcase}, #{url.to_s}, #{@asynchronous}, #{@user.to_n}, #{@password.to_n})`
# if there are no registered callbacks no point in setting the event
# handler
unless @callbacks.empty?
`#@native.onreadystatechange = #{callback}`
end
@opened = true
self
end
# Send the request with optional parameters.
#
# @param parameters [String, Hash] the data to send
#
# @return [Response] the response
def send(parameters = @parameters)
raise 'the request has not been opened' unless opened?
raise 'the request has already been sent' if sent?
# try to circumvent caching setting an If-Modified-Since header with a very
# old date
unless cacheable?
`#@native.setRequestHeader("If-Modified-Since", "Tue, 11 Sep 2001 12:46:00 GMT")`
end
@headers.each {|name, value|
`#@native.setRequestHeader(#{name.to_s}, #{value.to_s})`
}
if @content_type
header = @content_type
header += "; charset=#{@encoding}" if @encoding
`#@native.setRequestHeader('Content-Type', header)`
end
if binary?
if Buffer.supported?
`#@native.responseType = 'arraybuffer'`
else
`#@native.overrideMimeType('text/plain; charset=x-user-defined')`
end
end
if mime_type && !binary?
`#@native.overrideMimeType(#@mime_type)`
end
@sent = true
@response = Response.new(self)
if String === parameters
data = parameters
elsif (Hash === parameters && !parameters.empty?) || FormData === parameters
data = if Hash === parameters
if FormData.contain_files?(parameters)
FormData.build_form_data(parameters)
else
FormData.build_query(parameters)
end
else #if FormData === parameters
parameters
end
unless @content_type
if FormData === data
# I thought it's done this way, but it isn't. It actually is
# "multipart/form-data; boundary=-----------.......". Let's miss it
# purposefully, because it's filled in automatically in this example.
# `#@native.setRequestHeader('Content-Type', 'multipart/form-data')`
else
`#@native.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')`
end
end
data = data.to_n
else
data = `null`
end
`#@native.send(#{data})`
@response
end
# Abort the request.
def abort
`#@native.abort()`
end
private
def callback
-> event {
state = STATES[`#@native.readyState`]
res = response
@callbacks[state].each { |b| b.(res) }
if state == :complete
@completed = true
@callbacks[res.status.code].each { |b| b.(res) }
if res.success?
@callbacks[:success].each { |b| b.(res) }
else
@callbacks[:failure].each { |b| b.(res) }
end
end
}
end
end
end; end