#= require ./record u = up.util ###** Instances of `up.Request` normalizes properties of an [`AJAX request`](/up.request) such as the requested URL, form parameters and HTTP method. @class up.Request ### class up.Request extends up.Record ###** The HTTP method for the request. @property up.Request#method @param {string} method @stable ### ###** The URL for the request. @property up.Request#url @param {string} url @stable ### ###** Parameters that should be sent as the request's payload. Parameters may be passed as one of the following forms: 1. An object where keys are param names and the values are param values 2. An array of `{ name: 'param-name', value: 'param-value' }` objects 3. A [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object @property up.Request#data @param {String} data @stable ### ###** The CSS selector that will be sent as an [`X-Up-Target` header](/up.protocol#optimizing-responses). @property up.Request#target @param {string} target @stable ### ###** The CSS selector that will be sent as an [`X-Up-Fail-Target` header](/up.protocol#optimizing-responses). @property up.Request#failTarget @param {string} failTarget @stable ### ###** An object of additional HTTP headers. @property up.Request#headers @param {object} headers @stable ### ###** A timeout in milliseconds. If [`up.proxy.config.maxRequests`](/up.proxy.config#config.maxRequests) is set, the timeout will not include the time spent waiting in the queue. @property up.Request#timeout @param {object|undefined} timeout @stable ### fields: -> [ 'method', 'url', 'data', 'target', 'failTarget', 'headers', 'timeout' ] ###** @constructor up.Request @param {string} [attributes] ### constructor: (options) -> super(options) @normalize() normalize: => @method = u.normalizeMethod(@method) @headers ||= {} @extractHashFromUrl() if u.methodAllowsPayload(@method) @transferSearchToData() else @transferDataToUrl() extractHashFromUrl: => urlParts = u.parseUrl(@url) # Remember the #hash for later revealing. # It will be lost during normalization. @hash = urlParts.hash @url = u.normalizeUrl(urlParts, hash: false) transferDataToUrl: => if @data && !u.isFormData(@data) # GET methods are not allowed to have a payload, so we transfer { data } params to the URL. query = u.requestDataAsQuery(@data) separator = if u.contains(@url, '?') then '&' else '?' @url += separator + query # Now that we have transfered the params into the URL, we delete them from the { data } option. @data = undefined transferSearchToData: => urlParts = u.parseUrl(@url) query = urlParts.search if query @data = u.mergeRequestData(@data, query) @url = u.normalizeUrl(urlParts, search: false) isSafe: => up.proxy.isSafeMethod(@method) send: => # We will modify this request below. # This would confuse API clients and cache key logic in up.proxy. new Promise (resolve, reject) => xhr = new XMLHttpRequest() xhrHeaders = u.copy(@headers) xhrData = @data xhrMethod = @method xhrUrl = @url [xhrMethod, xhrData] = up.proxy.wrapMethod(xhrMethod, xhrData) if u.isFormData(xhrData) delete xhrHeaders['Content-Type'] # let the browser set the content type else if u.isPresent(xhrData) xhrData = u.requestDataAsQuery(xhrData, purpose: 'form') xhrHeaders['Content-Type'] = 'application/x-www-form-urlencoded' else # XMLHttpRequest expects null for an empty body xhrData = null xhrHeaders[up.protocol.config.targetHeader] = @target if @target xhrHeaders[up.protocol.config.failTargetHeader] = @failTarget if @failTarget xhrHeaders['X-Requested-With'] ||= 'XMLHttpRequest' unless @isCrossDomain() if csrfToken = @csrfToken() xhrHeaders[up.protocol.config.csrfHeader] = csrfToken xhr.open(xhrMethod, xhrUrl) for header, value of xhrHeaders xhr.setRequestHeader(header, value) resolveWithResponse = => response = @buildResponse(xhr) if response.isSuccess() resolve(response) else reject(response) # Convert from XHR API to promise API xhr.onload = resolveWithResponse xhr.onerror = resolveWithResponse xhr.ontimeout = resolveWithResponse xhr.timeout = @timeout if @timeout xhr.send(xhrData) navigate: => # GET forms cannot have an URL with a query section in their [action] attribute. # The query section would be overridden by the serialized input values on submission. @transferSearchToData() $form = $('
') addField = (field) -> $('').attr(field).appendTo($form) if @method == 'GET' formMethod = 'GET' else # Browser forms can only have GET or POST methods. # When we want to make a request with another method, most backend # frameworks allow to pass the method as a param. addField(name: up.protocol.config.methodParam, value: @method) formMethod = 'POST' $form.attr(method: formMethod, action: @url) if (csrfParam = up.protocol.csrfParam()) && (csrfToken = @csrfToken()) addField(name: csrfParam, value: csrfToken) # @data will be undefined for GET requests, since we have already # transfered all params to the URL during normalize(). u.each u.requestDataAsArray(@data), addField $form.hide().appendTo('body') up.browser.submitForm($form) # Returns a csrfToken if this request requires it csrfToken: => if !@isSafe() && !@isCrossDomain() up.protocol.csrfToken() isCrossDomain: => u.isCrossDomain(@url) buildResponse: (xhr) => responseAttrs = method: @method url: @url text: xhr.responseText status: xhr.status request: @ xhr: xhr if urlFromServer = up.protocol.locationFromXhr(xhr) responseAttrs.url = urlFromServer # If the server changes a URL, it is expected to signal a new method as well. responseAttrs.method = up.protocol.methodFromXhr(xhr) ? 'GET' responseAttrs.title = up.protocol.titleFromXhr(xhr) new up.Response(responseAttrs) isCachable: => @isSafe() && !u.isFormData(@data) cacheKey: => [@url, @method, u.requestDataAsQuery(@data), @target].join('|') @wrap: (object) -> if object instanceof @ # This object has gone through instantiation and normalization before. object else new @(object)