diff options
| author | Misko Hevery | 2012-03-23 14:03:24 -0700 | 
|---|---|---|
| committer | Misko Hevery | 2012-03-28 11:16:35 -0700 | 
| commit | 2430f52bb97fa9d682e5f028c977c5bf94c5ec38 (patch) | |
| tree | e7529b741d70199f36d52090b430510bad07f233 /src/ng/http.js | |
| parent | 944098a4e0f753f06b40c73ca3e79991cec6c2e2 (diff) | |
| download | angular.js-2430f52bb97fa9d682e5f028c977c5bf94c5ec38.tar.bz2 | |
chore(module): move files around in preparation for more modules
Diffstat (limited to 'src/ng/http.js')
| -rw-r--r-- | src/ng/http.js | 743 | 
1 files changed, 743 insertions, 0 deletions
| diff --git a/src/ng/http.js b/src/ng/http.js new file mode 100644 index 00000000..c2cbd161 --- /dev/null +++ b/src/ng/http.js @@ -0,0 +1,743 @@ +'use strict'; +'use strict'; + +/** + * Parse headers into key value object + * + * @param {string} headers Raw headers as a string + * @returns {Object} Parsed headers as key value object + */ +function parseHeaders(headers) { +  var parsed = {}, key, val, i; + +  if (!headers) return parsed; + +  forEach(headers.split('\n'), function(line) { +    i = line.indexOf(':'); +    key = lowercase(trim(line.substr(0, i))); +    val = trim(line.substr(i + 1)); + +    if (key) { +      if (parsed[key]) { +        parsed[key] += ', ' + val; +      } else { +        parsed[key] = val; +      } +    } +  }); + +  return parsed; +} + + +/** + * Returns a function that provides access to parsed headers. + * + * Headers are lazy parsed when first requested. + * @see parseHeaders + * + * @param {(string|Object)} headers Headers to provide access to. + * @returns {function(string=)} Returns a getter function which if called with: + * + *   - if called with single an argument returns a single header value or null + *   - if called with no arguments returns an object containing all headers. + */ +function headersGetter(headers) { +  var headersObj = isObject(headers) ? headers : undefined; + +  return function(name) { +    if (!headersObj) headersObj =  parseHeaders(headers); + +    if (name) { +      return headersObj[lowercase(name)] || null; +    } + +    return headersObj; +  }; +} + + +/** + * Chain all given functions + * + * This function is used for both request and response transforming + * + * @param {*} data Data to transform. + * @param {function(string=)} headers Http headers getter fn. + * @param {(function|Array.<function>)} fns Function or an array of functions. + * @returns {*} Transformed data. + */ +function transformData(data, headers, fns) { +  if (isFunction(fns)) +    return fns(data, headers); + +  forEach(fns, function(fn) { +    data = fn(data, headers); +  }); + +  return data; +} + + +function isSuccess(status) { +  return 200 <= status && status < 300; +} + + +function $HttpProvider() { +  var JSON_START = /^\s*(\[|\{[^\{])/, +      JSON_END = /[\}\]]\s*$/, +      PROTECTION_PREFIX = /^\)\]\}',?\n/; + +  var $config = this.defaults = { +    // transform incoming response data +    transformResponse: function(data) { +      if (isString(data)) { +        // strip json vulnerability protection prefix +        data = data.replace(PROTECTION_PREFIX, ''); +        if (JSON_START.test(data) && JSON_END.test(data)) +          data = fromJson(data, true); +      } +      return data; +    }, + +    // transform outgoing request data +    transformRequest: function(d) { +      return isObject(d) && !isFile(d) ? toJson(d) : d; +    }, + +    // default headers +    headers: { +      common: { +        'Accept': 'application/json, text/plain, */*', +        'X-Requested-With': 'XMLHttpRequest' +      }, +      post: {'Content-Type': 'application/json'}, +      put:  {'Content-Type': 'application/json'} +    } +  }; + +  var providerResponseInterceptors = this.responseInterceptors = []; + +  this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', +      function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { + +    var defaultCache = $cacheFactory('$http'), +        responseInterceptors = []; + +    forEach(providerResponseInterceptors, function(interceptor) { +      responseInterceptors.push( +          isString(interceptor) +              ? $injector.get(interceptor) +              : $injector.invoke(interceptor) +      ); +    }); + + +    /** +     * @ngdoc function +     * @name angular.module.ng.$http +     * @requires $httpBacked +     * @requires $browser +     * @requires $cacheFactory +     * @requires $rootScope +     * @requires $q +     * @requires $injector +     * +     * @description +     * The `$http` service is a core Angular service that facilitates communication with the remote +     * HTTP servers via browser's {@link https://developer.mozilla.org/en/xmlhttprequest +     * XMLHttpRequest} object or via {@link http://en.wikipedia.org/wiki/JSONP JSONP}. +     * +     * For unit testing applications that use `$http` service, see +     * {@link angular.module.ngMock.$httpBackend $httpBackend mock}. +     * +     * For a higher level of abstraction, please check out the {@link angular.module.ng.$resource +     * $resource} service. +     * +     * The $http API is based on the {@link angular.module.ng.$q deferred/promise APIs} exposed by +     * the $q service. While for simple usage patters this doesn't matter much, for advanced usage, +     * it is important to familiarize yourself with these apis and guarantees they provide. +     * +     * +     * # General usage +     * The `$http` service is a function which takes a single argument — a configuration object — +     * that is used to generate an http request and returns  a {@link angular.module.ng.$q promise} +     * with two $http specific methods: `success` and `error`. +     * +     * <pre> +     *   $http({method: 'GET', url: '/someUrl'}). +     *     success(function(data, status, headers, config) { +     *       // this callback will be called asynchronously +     *       // when the response is available +     *     }). +     *     error(function(data, status, headers, config) { +     *       // called asynchronously if an error occurs +     *       // or server returns response with status +     *       // code outside of the <200, 400) range +     *     }); +     * </pre> +     * +     * Since the returned value of calling the $http function is a Promise object, you can also use +     * the `then` method to register callbacks, and these callbacks will receive a single argument – +     * an object representing the response. See the api signature and type info below for more +     * details. +     * +     * +     * # Shortcut methods +     * +     * Since all invocation of the $http service require definition of the http method and url and +     * POST and PUT requests require response body/data to be provided as well, shortcut methods +     * were created to simplify using the api: +     * +     * <pre> +     *   $http.get('/someUrl').success(successCallback); +     *   $http.post('/someUrl', data).success(successCallback); +     * </pre> +     * +     * Complete list of shortcut methods: +     * +     * - {@link angular.module.ng.$http#get $http.get} +     * - {@link angular.module.ng.$http#head $http.head} +     * - {@link angular.module.ng.$http#post $http.post} +     * - {@link angular.module.ng.$http#put $http.put} +     * - {@link angular.module.ng.$http#delete $http.delete} +     * - {@link angular.module.ng.$http#jsonp $http.jsonp} +     * +     * +     * # Setting HTTP Headers +     * +     * The $http service will automatically add certain http headers to all requests. These defaults +     * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration +     * object, which currently contains this default configuration: +     * +     * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): +     *   - `Accept: application/json, text/plain, * / *` +     *   - `X-Requested-With: XMLHttpRequest` +     * - `$httpProvider.defaults.headers.post`: (header defaults for HTTP POST requests) +     *   - `Content-Type: application/json` +     * - `$httpProvider.defaults.headers.put` (header defaults for HTTP PUT requests) +     *   - `Content-Type: application/json` +     * +     * To add or overwrite these defaults, simply add or remove a property from this configuration +     * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object +     * with name equal to the lower-cased http method name, e.g. +     * `$httpProvider.defaults.headers.get['My-Header']='value'`. +     * +     * +     * # Transforming Requests and Responses +     * +     * Both requests and responses can be transformed using transform functions. By default, Angular +     * applies these transformations: +     * +     * Request transformations: +     * +     * - if the `data` property of the request config object contains an object, serialize it into +     *   JSON format. +     * +     * Response transformations: +     * +     *  - if XSRF prefix is detected, strip it (see Security Considerations section below) +     *  - if json response is detected, deserialize it using a JSON parser +     * +     * To override these transformation locally, specify transform functions as `transformRequest` +     * and/or `transformResponse` properties of the config object. To globally override the default +     * transforms, override the `$httpProvider.defaults.transformRequest` and +     * `$httpProvider.defaults.transformResponse` properties of the `$httpProvider`. +     * +     * +     * # Caching +     * +     * To enable caching set the configuration property `cache` to `true`. When the cache is +     * enabled, `$http` stores the response from the server in local cache. Next time the +     * response is served from the cache without sending a request to the server. +     * +     * Note that even if the response is served from cache, delivery of the data is asynchronous in +     * the same way that real requests are. +     * +     * If there are multiple GET requests for the same url that should be cached using the same +     * cache, but the cache is not populated yet, only one request to the server will be made and +     * the remaining requests will be fulfilled using the response for the first request. +     * +     * +     * # Response interceptors +     * +     * Before you start creating interceptors, be sure to understand the +     * {@link angular.module.ng.$q $q and deferred/promise APIs}. +     * +     * For purposes of global error handling, authentication or any kind of synchronous or +     * asynchronous preprocessing of received responses, it is desirable to be able to intercept +     * responses for http requests before they are handed over to the application code that +     * initiated these requests. The response interceptors leverage the {@link angular.module.ng.$q +     * promise apis} to fulfil this need for both synchronous and asynchronous preprocessing. +     * +     * The interceptors are service factories that are registered with the $httpProvider by +     * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and +     * injected with dependencies (if specified) and returns the interceptor  — a function that +     * takes a {@link angular.module.ng.$q promise} and returns the original or a new promise. +     * +     * <pre> +     *   // register the interceptor as a service +     *   $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { +     *     return function(promise) { +     *       return promise.then(function(response) { +     *         // do something on success +     *       }, function(response) { +     *         // do something on error +     *         if (canRecover(response)) { +     *           return responseOrNewPromise +     *         } +     *         return $q.reject(response); +     *       }); +     *     } +     *   }); +     * +     *   $httpProvider.responseInterceptors.push('myHttpInterceptor'); +     * +     * +     *   // register the interceptor via an anonymous factory +     *   $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) { +     *     return function(promise) { +     *       // same as above +     *     } +     *   }); +     * </pre> +     * +     * +     * # Security Considerations +     * +     * When designing web applications, consider security threats from: +     * +     * - {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx +     *   JSON Vulnerability} +     * - {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} +     * +     * Both server and the client must cooperate in order to eliminate these threats. Angular comes +     * pre-configured with strategies that address these issues, but for this to work backend server +     * cooperation is required. +     * +     * ## JSON Vulnerability Protection +     * +     * A {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx +     * JSON Vulnerability} allows third party web-site to turn your JSON resource URL into +     * {@link http://en.wikipedia.org/wiki/JSON#JSONP JSONP} request under some conditions. To +     * counter this your server can prefix all JSON requests with following string `")]}',\n"`. +     * Angular will automatically strip the prefix before processing it as JSON. +     * +     * For example if your server needs to return: +     * <pre> +     * ['one','two'] +     * </pre> +     * +     * which is vulnerable to attack, your server can return: +     * <pre> +     * )]}', +     * ['one','two'] +     * </pre> +     * +     * Angular will strip the prefix, before processing the JSON. +     * +     * +     * ## Cross Site Request Forgery (XSRF) Protection +     * +     * {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} is a technique by which +     * an unauthorized site can gain your user's private data. Angular provides following mechanism +     * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie +     * called `XSRF-TOKEN` and sets it as the HTTP header `X-XSRF-TOKEN`. Since only JavaScript that +     * runs on your domain could read the cookie, your server can be assured that the XHR came from +     * JavaScript running on your domain. +     * +     * To take advantage of this, your server needs to set a token in a JavaScript readable session +     * cookie called `XSRF-TOKEN` on first HTTP GET request. On subsequent non-GET requests the +     * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure +     * that only JavaScript running on your domain could have read the token. The token must be +     * unique for each user and must be verifiable by the server (to prevent the JavaScript making +     * up its own tokens). We recommend that the token is a digest of your site's authentication +     * cookie with {@link http://en.wikipedia.org/wiki/Rainbow_table salt for added security}. +     * +     * +     * @param {object} config Object describing the request to be made and how it should be +     *    processed. The object has following properties: +     * +     *    - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) +     *    - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. +     *    - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be turned to +     *      `?key1=value1&key2=value2` after the url. If the value is not a string, it will be JSONified. +     *    - **data** – `{string|Object}` – Data to be sent as the request message data. +     *    - **headers** – `{Object}` – Map of strings representing HTTP headers to send to the server. +     *    - **transformRequest** – `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – +     *      transform function or an array of such functions. The transform function takes the http +     *      request body and headers and returns its transformed (typically serialized) version. +     *    - **transformResponse** – `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – +     *      transform function or an array of such functions. The transform function takes the http +     *      response body and headers and returns its transformed (typically deserialized) version. +     *    - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the +     *      GET request, otherwise if a cache instance built with +     *      {@link angular.module.ng.$cacheFactory $cacheFactory}, this cache will be used for +     *      caching. +     *    - **timeout** – `{number}` – timeout in milliseconds. +     * +     * @returns {HttpPromise} Returns a {@link angular.module.ng.$q promise} object with the +     *   standard `then` method and two http specific methods: `success` and `error`. The `then` +     *   method takes two arguments a success and an error callback which will be called with a +     *   response object. The `success` and `error` methods take a single argument - a function that +     *   will be called when the request succeeds or fails respectively. The arguments passed into +     *   these functions are destructured representation of the response object passed into the +     *   `then` method. The response object has these properties: +     * +     *   - **data** – `{string|Object}` – The response body transformed with the transform functions. +     *   - **status** – `{number}` – HTTP status code of the response. +     *   - **headers** – `{function([headerName])}` – Header getter function. +     *   - **config** – `{Object}` – The configuration object that was used to generate the request. +     * +     * @property {Array.<Object>} pendingRequests Array of config objects for currently pending +     *   requests. This is primarily meant to be used for debugging purposes. +     * +     * +     * @example +        <doc:example> +          <doc:source jsfiddle="false"> +            <script> +              function FetchCtrl($scope, $http) { +                $scope.method = 'GET'; +                $scope.url = 'examples/http-hello.html'; + +                $scope.fetch = function() { +                  $scope.code = null; +                  $scope.response = null; + +                  $http({method: $scope.method, url: $scope.url}). +                    success(function(data, status) { +                      $scope.status = status; +                      $scope.data = data; +                    }). +                    error(function(data, status) { +                      $scope.data = data || "Request failed"; +                      $scope.status = status; +                  }); +                }; + +                $scope.updateModel = function(method, url) { +                  $scope.method = method; +                  $scope.url = url; +                }; +              } +            </script> +            <div ng-controller="FetchCtrl"> +              <select ng-model="method"> +                <option>GET</option> +                <option>JSONP</option> +              </select> +              <input type="text" ng-model="url" size="80"/> +              <button ng-click="fetch()">fetch</button><br> +              <button ng-click="updateModel('GET', 'examples/http-hello.html')">Sample GET</button> +              <button ng-click="updateModel('JSONP', 'http://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">Sample JSONP</button> +              <button ng-click="updateModel('JSONP', 'http://angularjs.org/doesntexist&callback=JSON_CALLBACK')">Invalid JSONP</button> +              <pre>http status code: {{status}}</pre> +              <pre>http response data: {{data}}</pre> +            </div> +          </doc:source> +          <doc:scenario> +            it('should make an xhr GET request', function() { +              element(':button:contains("Sample GET")').click(); +              element(':button:contains("fetch")').click(); +              expect(binding('status')).toBe('200'); +              expect(binding('data')).toBe('Hello, $http!\n'); +            }); + +            it('should make a JSONP request to angularjs.org', function() { +              element(':button:contains("Sample JSONP")').click(); +              element(':button:contains("fetch")').click(); +              expect(binding('status')).toBe('200'); +              expect(binding('data')).toMatch(/Super Hero!/); +            }); + +            it('should make JSONP request to invalid URL and invoke the error handler', +                function() { +              element(':button:contains("Invalid JSONP")').click(); +              element(':button:contains("fetch")').click(); +              expect(binding('status')).toBe('0'); +              expect(binding('data')).toBe('Request failed'); +            }); +          </doc:scenario> +        </doc:example> +     */ +    function $http(config) { +      config.method = uppercase(config.method); + +      var reqTransformFn = config.transformRequest || $config.transformRequest, +          respTransformFn = config.transformResponse || $config.transformResponse, +          defHeaders = $config.headers, +          reqHeaders = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, +              defHeaders.common, defHeaders[lowercase(config.method)], config.headers), +          reqData = transformData(config.data, headersGetter(reqHeaders), reqTransformFn), +          promise; + +      // strip content-type if data is undefined +      if (isUndefined(config.data)) { +        delete reqHeaders['Content-Type']; +      } + +      // send request +      promise = sendReq(config, reqData, reqHeaders); + + +      // transform future response +      promise = promise.then(transformResponse, transformResponse); + +      // apply interceptors +      forEach(responseInterceptors, function(interceptor) { +        promise = interceptor(promise); +      }); + +      promise.success = function(fn) { +        promise.then(function(response) { +          fn(response.data, response.status, response.headers, config); +        }); +        return promise; +      }; + +      promise.error = function(fn) { +        promise.then(null, function(response) { +          fn(response.data, response.status, response.headers, config); +        }); +        return promise; +      }; + +      return promise; + +      function transformResponse(response) { +        // make a copy since the response must be cacheable +        var resp = extend({}, response, { +          data: transformData(response.data, response.headers, respTransformFn) +        }); +        return (isSuccess(response.status)) +          ? resp +          : $q.reject(resp); +      } +    } + +    $http.pendingRequests = []; + +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#get +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `GET` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {Object=} config Optional configuration object +     * @returns {HttpPromise} Future object +     */ + +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#delete +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `DELETE` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {Object=} config Optional configuration object +     * @returns {HttpPromise} Future object +     */ + +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#head +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `HEAD` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {Object=} config Optional configuration object +     * @returns {XhrFuture} Future object +     */ + +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#jsonp +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `JSONP` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request. +     *                     Should contain `JSON_CALLBACK` string. +     * @param {Object=} config Optional configuration object +     * @returns {XhrFuture} Future object +     */ +    createShortMethods('get', 'delete', 'head', 'jsonp'); + +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#post +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `POST` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {*} data Request content +     * @param {Object=} config Optional configuration object +     * @returns {HttpPromise} Future object +     */ + +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#put +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `PUT` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {*} data Request content +     * @param {Object=} config Optional configuration object +     * @returns {XhrFuture} Future object +     */ +    createShortMethodsWithData('post', 'put'); + + +    return $http; + + +    function createShortMethods(names) { +      forEach(arguments, function(name) { +        $http[name] = function(url, config) { +          return $http(extend(config || {}, { +            method: name, +            url: url +          })); +        }; +      }); +    } + + +    function createShortMethodsWithData(name) { +      forEach(arguments, function(name) { +        $http[name] = function(url, data, config) { +          return $http(extend(config || {}, { +            method: name, +            url: url, +            data: data +          })); +        }; +      }); +    } + + +    /** +     * Makes the request +     * +     * !!! ACCESSES CLOSURE VARS: +     * $httpBackend, $config, $log, $rootScope, defaultCache, $http.pendingRequests +     */ +    function sendReq(config, reqData, reqHeaders) { +      var deferred = $q.defer(), +          promise = deferred.promise, +          cache, +          cachedResp, +          url = buildUrl(config.url, config.params); + +      $http.pendingRequests.push(config); +      promise.then(removePendingReq, removePendingReq); + + +      if (config.cache && config.method == 'GET') { +        cache = isObject(config.cache) ? config.cache : defaultCache; +      } + +      if (cache) { +        cachedResp = cache.get(url); +        if (cachedResp) { +          if (cachedResp.then) { +            // cached request has already been sent, but there is no response yet +            cachedResp.then(removePendingReq, removePendingReq); +            return cachedResp; +          } else { +            // serving from cache +            if (isArray(cachedResp)) { +              resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2])); +            } else { +              resolvePromise(cachedResp, 200, {}); +            } +          } +        } else { +          // put the promise for the non-transformed response into cache as a placeholder +          cache.put(url, promise); +        } +      } + +      // if we won't have the response in cache, send the request to the backend +      if (!cachedResp) { +        $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout); +      } + +      return promise; + + +      /** +       * Callback registered to $httpBackend(): +       *  - caches the response if desired +       *  - resolves the raw $http promise +       *  - calls $apply +       */ +      function done(status, response, headersString) { +        if (cache) { +          if (isSuccess(status)) { +            cache.put(url, [status, response, parseHeaders(headersString)]); +          } else { +            // remove promise from the cache +            cache.remove(url); +          } +        } + +        resolvePromise(response, status, headersString); +        $rootScope.$apply(); +      } + + +      /** +       * Resolves the raw $http promise. +       */ +      function resolvePromise(response, status, headers) { +        // normalize internal statuses to 0 +        status = Math.max(status, 0); + +        (isSuccess(status) ? deferred.resolve : deferred.reject)({ +          data: response, +          status: status, +          headers: headersGetter(headers), +          config: config +        }); +      } + + +      function removePendingReq() { +        var idx = indexOf($http.pendingRequests, config); +        if (idx !== -1) $http.pendingRequests.splice(idx, 1); +      } +    } + + +    function buildUrl(url, params) { +          if (!params) return url; +          var parts = []; +          forEachSorted(params, function(value, key) { +            if (value == null || value == undefined) return; +            if (isObject(value)) { +              value = toJson(value); +            } +            parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); +          }); +          return url + ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); +        } + + +  }]; +} | 
