text: /**
   * Copyright (c) 2015-present, Parse, LLC.
   * All rights reserved.
   *
   * This source code is licensed under the BSD-style license found in the
   * LICENSE file in the root directory of this source tree. An additional grant
   * of patent rights can be found in the PATENTS file in the same directory.
   *
   * @flow
   */
  /* global XMLHttpRequest, XDomainRequest */
  import CoreManager from './CoreManager';
  import ParseError from './ParseError';
  
  export type RequestOptions = {
    useMasterKey?: boolean;
    sessionToken?: string;
    installationId?: string;
    returnStatus?: boolean;
    batchSize?: number;
    include?: any;
    progress?: any;
  };
  
  export type FullOptions = {
    success?: any;
    error?: any;
    useMasterKey?: boolean;
    sessionToken?: string;
    installationId?: string;
    progress?: any;
  };
  
  let XHR = null;
  if (typeof XMLHttpRequest !== 'undefined') {
    XHR = XMLHttpRequest;
  }
  if (process.env.PARSE_BUILD === 'node') {
    XHR = require('xmlhttprequest').XMLHttpRequest;
  }
  
  let useXDomainRequest = false;
  if (typeof XDomainRequest !== 'undefined' &&
      !('withCredentials' in new XMLHttpRequest())) {
    useXDomainRequest = true;
  }
  
  function ajaxIE9(method: string, url: string, data: any, options?: FullOptions) {
    return new Promise((resolve, reject) => {
      const xdr = new XDomainRequest();
      xdr.onload = function() {
        let response;
        try {
          response = JSON.parse(xdr.responseText);
        } catch (e) {
          reject(e);
        }
        if (response) {
          resolve({ response });
        }
      };
      xdr.onerror = xdr.ontimeout = function() {
        // Let's fake a real error message.
        const fakeResponse = {
          responseText: JSON.stringify({
            code: ParseError.X_DOMAIN_REQUEST,
            error: 'IE\'s XDomainRequest does not supply error info.'
          })
        };
        reject(fakeResponse);
      };
      xdr.onprogress = function() {
        if(options && typeof options.progress === 'function') {
          options.progress(xdr.responseText);
        }
      };
      xdr.open(method, url);
      xdr.send(data);
    });
  }
  
  const RESTController = {
    ajax(method: string, url: string, data: any, headers?: any, options?: FullOptions) {
      if (useXDomainRequest) {
        return ajaxIE9(method, url, data, headers, options);
      }
  
      let res, rej;
      const promise = new Promise((resolve, reject) => { res = resolve; rej = reject; });
      promise.resolve = res;
      promise.reject = rej;
      let attempts = 0;
  
      const dispatch = function() {
        if (XHR == null) {
          throw new Error(
            'Cannot make a request: No definition of XMLHttpRequest was found.'
          );
        }
        let handled = false;
        const xhr = new XHR();
        xhr.onreadystatechange = function() {
          if (xhr.readyState !== 4 || handled) {
            return;
          }
          handled = true;
  
          if (xhr.status >= 200 && xhr.status < 300) {
            let response;
            try {
              response = JSON.parse(xhr.responseText);
  
              if (typeof xhr.getResponseHeader === 'function') {
                if ((xhr.getAllResponseHeaders() || '').includes('x-parse-job-status-id: ')) {
                  response = xhr.getResponseHeader('x-parse-job-status-id');
                }
              }
            } catch (e) {
              promise.reject(e.toString());
            }
            if (response) {
              promise.resolve({ response, status: xhr.status, xhr });
            }
          } else if (xhr.status >= 500 || xhr.status === 0) { // retry on 5XX or node-xmlhttprequest error
            if (++attempts < CoreManager.get('REQUEST_ATTEMPT_LIMIT')) {
              // Exponentially-growing random delay
              const delay = Math.round(
                Math.random() * 125 * Math.pow(2, attempts)
              );
              setTimeout(dispatch, delay);
            } else if (xhr.status === 0) {
              promise.reject('Unable to connect to the Parse API');
            } else {
              // After the retry limit is reached, fail
              promise.reject(xhr);
            }
          } else {
            promise.reject(xhr);
          }
        };
  
        headers = headers || {};
        if (typeof(headers['Content-Type']) !== 'string') {
          headers['Content-Type'] = 'text/plain'; // Avoid pre-flight
        }
        if (CoreManager.get('IS_NODE')) {
          headers['User-Agent'] = 'Parse/' + CoreManager.get('VERSION') +
            ' (NodeJS ' + process.versions.node + ')';
        }
        if (CoreManager.get('SERVER_AUTH_TYPE') && CoreManager.get('SERVER_AUTH_TOKEN')) {
          headers['Authorization'] = CoreManager.get('SERVER_AUTH_TYPE') + ' ' + CoreManager.get('SERVER_AUTH_TOKEN');
        }
  
        if(options && typeof options.progress === 'function') {
          if (xhr.upload) {
            xhr.upload.addEventListener('progress', (oEvent) => {
              if (oEvent.lengthComputable) {
                options.progress(oEvent.loaded / oEvent.total);
              } else {
                options.progress(null);
              }
            });
          } else if (xhr.addEventListener) {
            xhr.addEventListener('progress', (oEvent) => {
              if (oEvent.lengthComputable) {
                options.progress(oEvent.loaded / oEvent.total);
              } else {
                options.progress(null);
              }
            });
          }
        }
  
        xhr.open(method, url, true);
  
        for (const h in headers) {
          xhr.setRequestHeader(h, headers[h]);
        }
        xhr.send(data);
      }
      dispatch();
  
      return promise;
    },
  
    request(method: string, path: string, data: mixed, options?: RequestOptions) {
      options = options || {};
      let url = CoreManager.get('SERVER_URL');
      if (url[url.length - 1] !== '/') {
        url += '/';
      }
      url += path;
  
      const payload = {};
      if (data && typeof data === 'object') {
        for (const k in data) {
          payload[k] = data[k];
        }
      }
  
      if (method !== 'POST') {
        payload._method = method;
        method = 'POST';
      }
  
      payload._ApplicationId = CoreManager.get('APPLICATION_ID');
      const jsKey = CoreManager.get('JAVASCRIPT_KEY');
      if (jsKey) {
        payload._JavaScriptKey = jsKey;
      }
      payload._ClientVersion = CoreManager.get('VERSION');
  
      let useMasterKey = options.useMasterKey;
      if (typeof useMasterKey === 'undefined') {
        useMasterKey = CoreManager.get('USE_MASTER_KEY');
      }
      if (useMasterKey) {
        if (CoreManager.get('MASTER_KEY')) {
          delete payload._JavaScriptKey;
          payload._MasterKey = CoreManager.get('MASTER_KEY');
        } else {
          throw new Error('Cannot use the Master Key, it has not been provided.');
        }
      }
  
      if (CoreManager.get('FORCE_REVOCABLE_SESSION')) {
        payload._RevocableSession = '1';
      }
  
      const installationId = options.installationId;
      let installationIdPromise;
      if (installationId && typeof installationId === 'string') {
        installationIdPromise = Promise.resolve(installationId);
      } else {
        const installationController = CoreManager.getInstallationController();
        installationIdPromise = installationController.currentInstallationId();
      }
  
      return installationIdPromise.then((iid) => {
        payload._InstallationId = iid;
        const userController = CoreManager.getUserController();
        if (options && typeof options.sessionToken === 'string') {
          return Promise.resolve(options.sessionToken);
        } else if (userController) {
          return userController.currentUserAsync().then((user) => {
            if (user) {
              return Promise.resolve(user.getSessionToken());
            }
            return Promise.resolve(null);
          });
        }
        return Promise.resolve(null);
      }).then((token) => {
        if (token) {
          payload._SessionToken = token;
        }
  
        const payloadString = JSON.stringify(payload);
        return RESTController.ajax(method, url, payloadString, {}, options).then(({ response, status })=>{
          if (options.returnStatus) {
            return { ...response, _status: status };
          } else {
            return response;
          }
        });
      }).catch(function(response: { responseText: string }) {
        // Transform the error into an instance of ParseError by trying to parse
        // the error string as JSON
        let error;
        if (response && response.responseText) {
          try {
            const errorJSON = JSON.parse(response.responseText);
            error = new ParseError(errorJSON.code, errorJSON.error);
          } catch (e) {
            // If we fail to parse the error text, that's okay.
            error = new ParseError(
              ParseError.INVALID_JSON,
              'Received an error with invalid JSON from Parse: ' +
                response.responseText
            );
          }
        } else {
          error = new ParseError(
            ParseError.CONNECTION_FAILED,
            'XMLHttpRequest failed: ' + JSON.stringify(response)
          );
        }
  
        return Promise.reject(error);
      });
    },
  
    _setXHR(xhr: any) {
      XHR = xhr;
    }
  }
  
  module.exports = RESTController;
  