motion/http.rb in bubble-wrap-1.2.0 vs motion/http.rb in bubble-wrap-1.3.0.osx

- old
+ new

@@ -25,373 +25,9 @@ options[:action] = block if block HTTP::Query.new(url, http_verb, options) end end - - # Response class wrapping the results of a Query's response - class Response - attr_reader :body - attr_reader :headers - attr_accessor :status_code, :status_description, :error_message - attr_reader :url - attr_reader :original_url - - def initialize(values={}) - self.update(values) - end - - def update(values) - values.each do |k,v| - self.instance_variable_set("@#{k}", v) - end - update_status_description - end - - def ok? - status_code.to_s =~ /20\d/ ? true : false - end - - def to_s - "#<#{self.class}:#{self.object_id} - url: #{self.url}, body: #{self.body}, headers: #{self.headers}, status code: #{self.status_code}, error message: #{self.error_message} >" - end - alias description to_s - - def update_status_description - @status_description = status_code.nil? ? nil : NSHTTPURLResponse.localizedStringForStatusCode(status_code) - end - end - - # Class wrapping NSConnection and often used indirectly by the BubbleWrap::HTTP module methods. - class Query - attr_accessor :request - attr_accessor :connection - attr_accessor :credentials # username & password has a hash - attr_accessor :proxy_credential # credential supplied to proxy servers - attr_accessor :post_data - attr_reader :method - - attr_reader :response - attr_reader :status_code - attr_reader :response_headers - attr_reader :response_size - attr_reader :options - CLRF = "\r\n" - # ==== Parameters - # url<String>:: url of the resource to download - # http_method<Symbol>:: Value representing the HTTP method to use - # options<Hash>:: optional options used for the query - # - # ==== Options - # :payload<String> - data to pass to a POST, PUT, DELETE query. - # :delegator - Proc, class or object to call when the file is downloaded. - # a proc will receive a Response object while the passed object - # will receive the handle_query_response method - # :headers<Hash> - headers send with the request - # :cookies<Boolean> - Set whether cookies should be sent with request or not (Default: true) - # Anything else will be available via the options attribute reader. - # - def initialize(url_string, http_method = :get, options={}) - @method = http_method.upcase.to_s - @delegator = options.delete(:action) || self - @payload = options.delete(:payload) - @files = options.delete(:files) - @boundary = options.delete(:boundary) || BW.create_uuid - @credentials = options.delete(:credentials) || {} - @credentials = {:username => '', :password => ''}.merge(@credentials) - @timeout = options.delete(:timeout) || 30.0 - @headers = escape_line_feeds(options.delete :headers) - @format = options.delete(:format) - @cache_policy = options.delete(:cache_policy) || NSURLRequestUseProtocolCachePolicy - @credential_persistence = options.delete(:credential_persistence) || NSURLCredentialPersistenceForSession - @cookies = options.key?(:cookies) ? options.delete(:cookies) : true - @options = options - @response = HTTP::Response.new - @follow_urls = options[:follow_urls] || true - - @url = create_url(url_string) - @body = create_request_body - @request = create_request - @original_url = @url.copy - - @connection = create_connection(request, self) - @connection.start - - UIApplication.sharedApplication.networkActivityIndicatorVisible = true if defined?(UIApplication) - end - - def to_s - "#<#{self.class}:#{self.object_id} - Method: #{@method}, url: #{@url.description}, body: #{@body.description}, Payload: #{@payload}, Headers: #{@headers} Credentials: #{@credentials}, Timeout: #{@timeout}, \ -Cache policy: #{@cache_policy}, response: #{@response.inspect} >" - end - alias description to_s - - def connection(connection, didReceiveResponse:response) - @status_code = response.statusCode - @response_headers = response.allHeaderFields - @response_size = response.expectedContentLength.to_f - end - - # This delegate method get called every time a chunk of data is being received - def connection(connection, didReceiveData:received_data) - @received_data ||= NSMutableData.new - @received_data.appendData(received_data) - - if download_progress = options[:download_progress] - download_progress.call(@received_data.length.to_f, response_size) - end - end - - def connection(connection, willSendRequest:request, redirectResponse:redirect_response) - # abort early if the user has explicitly disabled redirects - if @options[:no_redirect] and redirect_response then - return nil - end - @redirect_count ||= 0 - @redirect_count += 1 - log "##{@redirect_count} HTTP redirect_count: #{request.inspect} - #{self.description}" - - if @redirect_count >= 30 - @response.error_message = "Too many redirections" - @request.done_loading! - call_delegator_with_response - nil - else - @url = request.URL if @follow_urls - request - end - end - - def connection(connection, didFailWithError: error) - log "HTTP Connection to #{@url.absoluteString} failed #{error.localizedDescription}" - UIApplication.sharedApplication.networkActivityIndicatorVisible = false if defined?(UIApplication) - @request.done_loading! - @response.error_message = error.localizedDescription - call_delegator_with_response - end - - def connection(connection, didSendBodyData:sending, totalBytesWritten:written, totalBytesExpectedToWrite:expected) - if upload_progress = options[:upload_progress] - upload_progress.call(sending, written, expected) - end - end - - def connectionDidFinishLoading(connection) - UIApplication.sharedApplication.networkActivityIndicatorVisible = false if defined?(UIApplication) - @request.done_loading! - response_body = NSData.dataWithData(@received_data) if @received_data - @response.update(status_code: status_code, body: response_body, headers: response_headers, url: @url, original_url: @original_url) - - call_delegator_with_response - end - - def connection(connection, didReceiveAuthenticationChallenge:challenge) - if (challenge.previousFailureCount == 0) - if credentials[:username].to_s.empty? && credentials[:password].to_s.empty? - challenge.sender.continueWithoutCredentialForAuthenticationChallenge(challenge) - log 'Continue without credentials to get 401 status in response' - else - new_credential = NSURLCredential.credentialWithUser(credentials[:username], password:credentials[:password], persistence:@credential_persistence) - challenge.sender.useCredential(new_credential, forAuthenticationChallenge:challenge) - log "auth challenged, answered with credentials: #{credentials.inspect}" - end - else - challenge.sender.cancelAuthenticationChallenge(challenge) - log 'Auth Failed :(' - end - end - - - private - - def create_request - log "BubbleWrap::HTTP building a NSRequest for #{@url.description}" - - request = NSMutableURLRequest.requestWithURL(@url, - cachePolicy:@cache_policy, - timeoutInterval:@timeout) - request.setHTTPMethod(@method) - set_content_type - request.setAllHTTPHeaderFields(@headers) - request.setHTTPBody(@body) - request.setHTTPShouldHandleCookies(@cookies) - patch_nsurl_request(request) - - request - end - - def set_content_type - return if headers_provided? - return if (@method == "GET" || @method == "HEAD") - @headers ||= {} - @headers["Content-Type"] = case @format - when :json - "application/json" - when :xml - "application/xml" - when :text - "text/plain" - else - if @format == :form_data || @payload_or_files_were_appended - "multipart/form-data; boundary=#{@boundary}" - else - "application/x-www-form-urlencoded" - end - end - end - - def headers_provided? - @headers && @headers.keys.find {|k| k.downcase == 'content-type'} - end - - def create_request_body - return nil if (@method == "GET" || @method == "HEAD") - return nil unless (@payload || @files) - - body = NSMutableData.data - - append_payload(body) if @payload - append_files(body) if @files - append_body_boundary(body) if @payload_or_files_were_appended - - log "Built HTTP body: \n #{body.to_str}" - body - end - - def append_payload(body) - if @payload.is_a?(NSData) - body.appendData(@payload) - elsif @payload.is_a?(String) - body.appendData(@payload.dataUsingEncoding NSUTF8StringEncoding) - else - append_form_params(body) - end - body - end - - def append_form_params(body) - list = process_payload_hash(@payload) - list.each do |key, value| - s = "--#{@boundary}\r\n" - s += "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n" - s += value.to_s - s += "\r\n" - body.appendData(s.dataUsingEncoding NSUTF8StringEncoding) - end - @payload_or_files_were_appended = true - body - end - - def parse_file(key, value) - if value.is_a?(Hash) - raise InvalidFileError if value[:data].nil? - {data: value[:data], filename: value[:filename] || key} - else - {data: value, filename: key} - end - end - - def append_files(body) - @files.each do |key, value| - file = parse_file(key, value) - s = "--#{@boundary}\r\n" - s += "Content-Disposition: form-data; name=\"#{key}\"; filename=\"#{file[:filename]}\"\r\n" - s += "Content-Type: application/octet-stream\r\n\r\n" - file_data = NSMutableData.new - file_data.appendData(s.dataUsingEncoding NSUTF8StringEncoding) - file_data.appendData(file[:data]) - file_data.appendData("\r\n".dataUsingEncoding NSUTF8StringEncoding) - body.appendData(file_data) - end - @payload_or_files_were_appended = true - body - end - - def append_body_boundary(body) - body.appendData("--#{@boundary}--\r\n".dataUsingEncoding NSUTF8StringEncoding) - end - - def create_url(url_string) - url_string = url_string.stringByAddingPercentEscapesUsingEncoding NSUTF8StringEncoding - if (@method == "GET" || @method == "HEAD") && @payload - convert_payload_to_url if @payload.is_a?(Hash) - url_string += "?#{@payload}" - end - url = NSURL.URLWithString(url_string) - - validate_url(url) - url - end - - def validate_url(url) - if !NSURLConnection.canHandleRequest(NSURLRequest.requestWithURL(url)) - raise InvalidURLError, "Invalid URL provided (Make sure you include a valid URL scheme, e.g. http:// or similar)." - end - end - - def escape(string) - if string - CFURLCreateStringByAddingPercentEscapes nil, string.to_s, nil, "!*'();:@&=+$,/?%#[]", KCFStringEncodingUTF8 - end - end - - def convert_payload_to_url - params_array = process_payload_hash(@payload) - params_array.map! { |key, value| "#{escape key}=#{escape value}" } - @payload = params_array.join("&") - end - - def process_payload_hash(payload, prefix=nil) - list = [] - payload.each do |k,v| - if v.is_a?(Hash) - new_prefix = prefix ? "#{prefix}[#{k.to_s}]" : k.to_s - param = process_payload_hash(v, new_prefix) - list += param - elsif v.is_a?(Array) - v.each do |val| - param = prefix ? "#{prefix}[#{k.to_s}][]" : "#{k.to_s}[]" - list << [param, val] - end - else - param = prefix ? "#{prefix}[#{k.to_s}]" : k.to_s - list << [param, v] - end - end - list - end - - def log(message) - NSLog message if BubbleWrap.debug? - end - - def escape_line_feeds(hash) - return nil if hash.nil? - escaped_hash = {} - - hash.each{|k,v| escaped_hash[k] = v.gsub("\n", CLRF) if v } - escaped_hash - end - - def patch_nsurl_request(request) - request.instance_variable_set("@done_loading", false) - - def request.done_loading?; @done_loading; end - def request.done_loading!; @done_loading = true; end - end - - def call_delegator_with_response - if @delegator.respond_to?(:call) - @delegator.call( @response, self ) - end - end - - # This is a temporary method used for mocking. - def create_connection(request, delegate) - NSURLConnection.connectionWithRequest(request, delegate:delegate) - end - - end end end class InvalidURLError < StandardError; end class InvalidFileError < StandardError; end