<commit_before>'use strict';

// Intentionally use native-promise-only here... Other promise libraries (es6-promise)
// duck-punch the global Promise definition which messes up Angular 2 since it
// also duck-punches the global Promise definition. For now, keep native-promise-only.
var Promise = require("native-promise-only");
require('whatwg-fetch');
var EventEmitter = require('eventemitter2').EventEmitter2;
var copy = require('shallow-copy');

/**
 * The Formio interface class.
 *
 *   let formio = new Formio('https://examples.form.io/example');
 */
export class Formio {
  constructor(path, options = {}) {
    // Ensure we have an instance of Formio.
    if (!(this instanceof Formio)) { return new Formio(path); }

    // Initialize our variables.
    this.base = '';
    this.projectsUrl = '';
    this.projectUrl = '';
    this.projectId = '';
    this.formUrl = '';
    this.formsUrl = '';
    this.formId = '';
    this.submissionsUrl = '';
    this.submissionUrl = '';
    this.submissionId = '';
    this.actionsUrl = '';
    this.actionId = '';
    this.actionUrl = '';
    this.query = '';

    if (options.hasOwnProperty('base')) {
      this.base = options.base;
    }
    else if (Formio.baseUrl) {
      this.base = Formio.baseUrl;
    }
    else {
      this.base = window.location.href.match(/http[s]?:\/\/api./)[0];
    }

    if (!path) {
      // Allow user to create new projects if this was instantiated without
      // a url
      this.projectUrl = this.base + '/project';
      this.projectsUrl = this.base + '/project';
      this.projectId = false;
      this.query = '';
      return;
    }

    if (options.hasOwnProperty('project')) {
      this.projectUrl = options.project;
    }

    let project = this.projectUrl || Formio.projectUrl;

    // The baseURL is the same as the projectUrl. This is almost certainly against
    // the Open Source server.
    if (project && this.base === project) {
      this.noProject = true;
      this.projectUrl = this.base;
    }

    // Normalize to an absolute path.
    if ((path.indexOf('http') !== 0) && (path.indexOf('//') !== 0)) {
      path = this.base + path;
    }

    var hostparts = Formio.getUrlParts(path);
    var parts = [];
    var hostName = hostparts[1] + hostparts[2];
    path = hostparts.length > 3 ? hostparts[3] : '';
    var queryparts = path.split('?');
    if (queryparts.length > 1) {
      path = queryparts[0];
      this.query = '?' + queryparts[1];
    }

    // Register a specific path.
    var registerPath = (name, base) => {
      this[name + 'sUrl'] = base + '/' + name;
      var regex = new RegExp('\/' + name + '\/([^/]+)');
      if (path.search(regex) !== -1) {
        parts = path.match(regex);
        this[name + 'Url'] = parts ? (base + parts[0]) : '';
        this[name + 'Id'] = (parts.length > 1) ? parts[1] : '';
        base += parts[0];
      }
      return base;
    };

    // Register an array of items.
    var registerItems = function(items, base, staticBase) {
      for (var i in items) {
        if (items.hasOwnProperty(i)) {
          var item = items[i];
          if (item instanceof Array) {
            registerItems(item, base, true);
          }
          else {
            var newBase = registerPath(item, base);
            base = staticBase ? base : newBase;
          }
        }
      }
    };

    if (!this.projectUrl || (this.projectUrl === this.base)) {
      this.projectUrl = hostName;
    }

    if (!this.noProject) {
      // Determine the projectUrl and projectId
      if ((path.search(/(^|\/)(project)($|\/)/) !== -1)) {
        // Get project id as project/:projectId.
        registerItems(['project'], hostName);
      }
      else if (hostName === this.base) {
        // Get project id as first part of path (subdirectory).
        if (hostparts.length > 3 && path.split('/').length > 1) {
          var pathParts = path.split('/');
          pathParts.shift(); // Throw away the first /.
          this.projectId = pathParts.shift();
          path = '/' + pathParts.join('/');
          this.projectUrl = hostName + '/' + this.projectId;
        }
      }
      else {
        // Get project id from subdomain.
        if (hostparts.length > 2 && (hostparts[2].split('.').length > 2 || hostName.indexOf('localhost') !== -1)) {
          this.projectUrl = hostName;
          this.projectId = hostparts[2].split('.')[0];
        }
      }
      this.projectsUrl = this.projectsUrl || this.base + '/project';
    }

    // Configure Form urls and form ids.
    if ((path.search(/(^|\/)(project|form)($|\/)/) !== -1)) {
      registerItems(['form', ['submission', 'action']], this.projectUrl);
    }
    else {
      var subRegEx = new RegExp('\/(submission|action)($|\/.*)');
      var subs = path.match(subRegEx);
      this.pathType = (subs && (subs.length > 1)) ? subs[1] : '';
      path = path.replace(subRegEx, '');
      path = path.replace(/\/$/, '');
      this.formsUrl = this.projectUrl + '/form';
      this.formUrl = this.projectUrl + path;
      this.formId = path.replace(/^\/+|\/+$/g, '');
      var items = ['submission', 'action'];
      for (var i in items) {
        if (items.hasOwnProperty(i)) {
          var item = items[i];
          this[item + 'sUrl'] = this.projectUrl + path + '/' + item;
          if ((this.pathType === item) && (subs.length > 2) && subs[2]) {
            this[item + 'Id'] = subs[2].replace(/^\/+|\/+$/g, '');
            this[item + 'Url'] = this.projectUrl + path + subs[0];
          }
        }
      }
    }

    // Set the app url if it is not set.
    if (!Formio.projectUrlSet) {
      Formio.projectUrl = this.projectUrl;
    }
  }

  delete(type, opts) {
    var _id = type + 'Id';
    var _url = type + 'Url';
    if (!this[_id]) { Promise.reject('Nothing to delete'); }
    Formio.cache = {};
    return this.makeRequest(type, this[_url], 'delete', null, opts);
  }

  index(type, query, opts) {
    var _url = type + 'Url';
    query = query || '';
    if (query && typeof query === 'object') {
      query = '?' + Formio.serialize(query.params);
    }
    return this.makeRequest(type, this[_url] + query, 'get', null, opts);
  }

  save(type, data, opts) {
    var _id = type + 'Id';
    var _url = type + 'Url';
    var method = (this[_id] || data._id) ? 'put' : 'post';
    var reqUrl = this[_id] ? this[_url] : this[type + 'sUrl'];
    if (!this[_id] && data._id && (method === 'put')) {
      reqUrl += '/' + data._id;
    }
    Formio.cache = {};
    return this.makeRequest(type, reqUrl + this.query, method, data, opts);
  }

  load(type, query, opts) {
    var _id = type + 'Id';
    var _url = type + 'Url';
    if (query && typeof query === 'object') {
      query = Formio.serialize(query.params);
    }
    if (query) {
      query = this.query ? (this.query + '&' + query) : ('?' + query);
    }
    else {
      query = this.query;
    }
    if (!this[_id]) { return Promise.reject('Missing ' + _id); }
    return this.makeRequest(type, this[_url] + query, 'get', null, opts);
  }

  makeRequest(type, url, method, data, opts) {
    method = (method || 'GET').toUpperCase();
    if(!opts || typeof opts !== 'object') {
      opts = {};
    }

    var requestArgs = {
      formio: this,
      type: type,
      url: url,
      method: method,
      data: data,
      opts: opts
    };

    var request = Formio.pluginWait('preRequest', requestArgs)
      .then(function() {
        return Formio.pluginGet('request', requestArgs)
          .then(function(result) {
            if (result === null || result === undefined) {
              return Formio.request(url, method, data, opts.header, opts);
            }
            return result;
          });
      });

    return Formio.pluginAlter('wrapRequestPromise', request, requestArgs);
  }

  loadProject(query, opts) {
    return this.load('project', query, opts);
  }

  saveProject(data, opts) {
    return this.save('project', data, opts);
  }

  deleteProject(opts) {
    return this.delete('project', opts);
  }

  static loadProjects(query, opts) {
    query = query || '';
    if (typeof query === 'object') {
      query = '?' + serialize(query.params);
    }
    return Formio.makeStaticRequest(Formio.baseUrl + '/project' + query);
  }

  loadForm(query, opts) {
    return this.load('form', query, opts);
  }

  saveForm(data, opts) {
    return this.save('form', data, opts);
  }

  deleteForm(opts) {
    return this.delete('form', opts);
  }

  loadForms(query, opts) {
    return this.index('forms', query, opts);
  }

  loadSubmission(query, opts) {
    return this.load('submission', query, opts);
  }

  saveSubmission(data, opts) {
    return this.save('submission', data, opts);
  }

  deleteSubmission(opts) {
    return this.delete('submission', opts);
  }

  loadSubmissions(query, opts) {
    return this.index('submissions', query, opts);
  }

  loadAction(query, opts) {
    return this.load('action', query, opts);
  }

  saveAction(data, opts) {
    return this.save('action', data, opts);
  }

  deleteAction(opts) {
    return this.delete('action', opts);
  }

  loadActions(query, opts) {
    return this.index('actions', query, opts);
  }

  availableActions() {
    return this.makeRequest('availableActions', this.formUrl + '/actions');
  }

  actionInfo(name) {
    return this.makeRequest('actionInfo', this.formUrl + '/actions/' + name);
  }

  uploadFile(storage, file, fileName, dir, progressCallback, url) {
    var requestArgs = {
      provider: storage,
      method: 'upload',
      file: file,
      fileName: fileName,
      dir: dir
    }
    var request = Formio.pluginWait('preRequest', requestArgs)
      .then(function() {
        return Formio.pluginGet('fileRequest', requestArgs)
          .then(function(result) {
            if (storage && (result === null || result === undefined)) {
              if (Formio.providers.storage.hasOwnProperty(storage)) {
                var provider = new Formio.providers.storage[storage](this);
                return provider.uploadFile(file, fileName, dir, progressCallback, url);
              }
              else {
                throw('Storage provider not found');
              }
            }
            return result || {url: ''};
          }.bind(this));
      }.bind(this));

    return Formio.pluginAlter('wrapFileRequestPromise', request, requestArgs);
  }

  downloadFile(file) {
    var requestArgs = {
      method: 'download',
      file: file
    };

    var request = Formio.pluginWait('preRequest', requestArgs)
      .then(function() {
        return Formio.pluginGet('fileRequest', requestArgs)
          .then(function(result) {
            if (file.storage && (result === null || result === undefined)) {
              if (Formio.providers.storage.hasOwnProperty(file.storage)) {
                var provider = new Formio.providers.storage[file.storage](this);
                return provider.downloadFile(file);
              }
              else {
                throw('Storage provider not found');
              }
            }
            return result || {url: ''};
          }.bind(this));
      }.bind(this));

    return Formio.pluginAlter('wrapFileRequestPromise', request, requestArgs);
  }

  static getUrlParts(url) {
    var regex = '^(http[s]?:\\/\\/)';
    if (this.base && url.indexOf(this.base) === 0) {
      regex += '(' + this.base.replace(/^http[s]?:\/\//, '') + ')';
    }
    else {
      regex += '([^/]+)';
    }
    regex += '($|\\/.*)';
    return url.match(new RegExp(regex));
  }

  static serialize(obj) {
    var str = [];
    for(var p in obj)
      if (obj.hasOwnProperty(p)) {
        str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
      }
    return str.join("&");
  }

  static makeStaticRequest(url, method, data, opts) {
    method = (method || 'GET').toUpperCase();
    if(!opts || typeof opts !== 'object') {
      opts = {};
    }
    var requestArgs = {
      url: url,
      method: method,
      data: data
    };

    var request = Formio.pluginWait('preRequest', requestArgs)
      .then(function() {
        return Formio.pluginGet('staticRequest', requestArgs)
          .then(function(result) {
            if (result === null || result === undefined) {
              return Formio.request(url, method, data, opts.header, opts);
            }
            return result;
          });
      });

    return Formio.pluginAlter('wrapStaticRequestPromise', request, requestArgs);
  }

  static request(url, method, data, header, opts) {
    if (!url) {
      return Promise.reject('No url provided');
    }
    method = (method || 'GET').toUpperCase();

    // For reverse compatibility, if they provided the ignoreCache parameter,
    // then change it back to the options format where that is a parameter.
    if (typeof opts === 'boolean') {
      opts = {ignoreCache: opts};
    }
    if(!opts || typeof opts !== 'object') {
      opts = {};
    }

    var cacheKey = btoa(url);

    return new Promise(function(resolve, reject) {
      // Get the cached promise to save multiple loads.
      if (!opts.ignoreCache && method === 'GET' && Formio.cache.hasOwnProperty(cacheKey)) {
        return resolve(Formio.cache[cacheKey]);
      }

      resolve(new Promise(function(resolve, reject) {
        // Set up and fetch request
        var headers = header || new Headers({
            'Accept': 'application/json',
            'Content-type': 'application/json; charset=UTF-8'
          });
        var token = Formio.getToken();
        if (token && !opts.noToken) {
          headers.append('x-jwt-token', token);
        }

        var options = {
          method: method,
          headers: headers,
          mode: 'cors'
        };
        if (data) {
          options.body = JSON.stringify(data);
        }

        resolve(fetch(url, options));
      })
        .catch(function(err) {
          err.message = 'Could not connect to API server (' + err.message + ')';
          err.networkError = true;
          throw err;
        })
        .then(function(response) {
          if (!response.ok) {
            if (response.status === 440) {
              Formio.setToken(null);
              Formio.events.emit('formio.sessionExpired', response.body);
            }
            else if (response.status === 401) {
              Formio.events.emit('formio.unauthorized', response.body);
            }
            // Parse and return the error as a rejected promise to reject this promise
            return (response.headers.get('content-type').indexOf('application/json') !== -1 ?
              response.json() : response.text())
              .then(function(error){
                throw error;
              });
          }

          // Handle fetch results
          var token = response.headers.get('x-jwt-token');
          if (response.status >= 200 && response.status < 300 && token && token !== '') {
            Formio.setToken(token);
          }
          // 204 is no content. Don't try to .json() it.
          if (response.status === 204) {
            return {};
          }
          return (response.headers.get('content-type').indexOf('application/json') !== -1
              ? response.json()
              : response.text()
          ).then(function(result) {
            // Add some content-range metadata to the result here
            var range = response.headers.get('content-range');
            if (range && typeof result === 'object') {
              range = range.split('/');
              if(range[0] !== '*') {
                var skipLimit = range[0].split('-');
                result.skip = Number(skipLimit[0]);
                result.limit = skipLimit[1] - skipLimit[0] + 1;
              }
              result.serverCount = range[1] === '*' ? range[1] : Number(range[1]);
            }

            if (!opts.getHeaders) {
              return result;
            }

            var headers = {};
            response.headers.forEach(function(item, key) {
              headers[key] = item;
            });

            return new Promise(function(resolve, reject) {
              resolve({result: result, headers: headers});
            });
          });
        })
        .catch(function(err) {
          if (err === 'Bad Token') {
            Formio.setToken(null);
            Formio.events.emit('formio.badToken', err);
          }
          if (Formio.cache.hasOwnProperty(cacheKey)) {
            // Remove failed promises from cache
            delete Formio.cache[cacheKey];
          }
          // Propagate error so client can handle accordingly
          throw err;
        }));
    })
      .then(function(result) {
        // Save the cache
        if (method === 'GET') {
          Formio.cache[cacheKey] = Promise.resolve(result);
        }

        // Shallow copy result so modifications don't end up in cache
        if(Array.isArray(result)) {
          var resultCopy = result.map(copy);
          resultCopy.skip = result.skip;
          resultCopy.limit = result.limit;
          resultCopy.serverCount = result.serverCount;
          return resultCopy;
        }
        return copy(result);
      });
  }

  static setToken(token) {
    token = token || '';
    if (token === this.token) { return; }
    this.token = token;
    if (!token) {
      Formio.setUser(null);
      // iOS in private browse mode will throw an error but we can't detect ahead of time that we are in private mode.
      try {
        return localStorage.removeItem('formioToken');
      }
      catch(err) {
        return;
      }
    }
    // iOS in private browse mode will throw an error but we can't detect ahead of time that we are in private mode.
    try {
      localStorage.setItem('formioToken', token);
    }
    catch(err) {
      // Do nothing.
    }
    return Formio.currentUser(); // Run this so user is updated if null
  }

  static getToken() {
    if (this.token) { return this.token; }
    try {
      var token = localStorage.getItem('formioToken') || '';
      this.token = token;
      return token;
    }
    catch (e) {
      return '';
    }
  }

  static setUser(user) {
    if (!user) {
      this.setToken(null);
      // iOS in private browse mode will throw an error but we can't detect ahead of time that we are in private mode.
      try {
        return localStorage.removeItem('formioUser');
      }
      catch(err) {
        return;
      }
    }
    // iOS in private browse mode will throw an error but we can't detect ahead of time that we are in private mode.
    try {
      localStorage.setItem('formioUser', JSON.stringify(user));
    }
    catch(err) {
      // Do nothing.
    }
  }

  static getUser() {
    try {
      return JSON.parse(localStorage.getItem('formioUser') || null);
    }
    catch (e) {
      return;
    }
  }

  static setBaseUrl(url) {
    Formio.baseUrl = url;
    if (!Formio.projectUrlSet) {
      Formio.projectUrl = url;
    }
  }

  static getBaseUrl() {
    return Formio.baseUrl;
  }

  static setApiUrl(url) {
    return Formio.setBaseUrl(url);
  }

  static getApiUrl() {
    return Formio.getBaseUrl();
  }

  static setAppUrl(url) {
    console.warn('Formio.setAppUrl() is deprecated. Use Form.setProjectUrl instead.')
    Formio.projectUrl = url;
    Formio.projectUrlSet = true;
  }

  static setProjectUrl(url) {
    Formio.projectUrl = url;
    Formio.projectUrlSet = true;
  }

  static getAppUrl() {
    console.warn('Formio.getAppUrl() is deprecated. Use Form.getProjectUrl instead.')
    return Formio.projectUrl;
  }

  static getProjectUrl() {
    return Formio.projectUrl;<commit_msg>Fix spelling.<commit_after>'use strict';

// Intentionally use native-promise-only here... Other promise libraries (es6-promise)
// duck-punch the global Promise definition which messes up Angular 2 since it
// also duck-punches the global Promise definition. For now, keep native-promise-only.
var Promise = require("native-promise-only");
require('whatwg-fetch');
var EventEmitter = require('eventemitter2').EventEmitter2;
var copy = require('shallow-copy');

/**
 * The Formio interface class.
 *
 *   let formio = new Formio('https://examples.form.io/example');
 */
export class Formio {
  constructor(path, options = {}) {
    // Ensure we have an instance of Formio.
    if (!(this instanceof Formio)) { return new Formio(path); }

    // Initialize our variables.
    this.base = '';
    this.projectsUrl = '';
    this.projectUrl = '';
    this.projectId = '';
    this.formUrl = '';
    this.formsUrl = '';
    this.formId = '';
    this.submissionsUrl = '';
    this.submissionUrl = '';
    this.submissionId = '';
    this.actionsUrl = '';
    this.actionId = '';
    this.actionUrl = '';
    this.query = '';

    if (options.hasOwnProperty('base')) {
      this.base = options.base;
    }
    else if (Formio.baseUrl) {
      this.base = Formio.baseUrl;
    }
    else {
      this.base = window.location.href.match(/http[s]?:\/\/api./)[0];
    }

    if (!path) {
      // Allow user to create new projects if this was instantiated without
      // a url
      this.projectUrl = this.base + '/project';
      this.projectsUrl = this.base + '/project';
      this.projectId = false;
      this.query = '';
      return;
    }

    if (options.hasOwnProperty('project')) {
      this.projectUrl = options.project;
    }

    let project = this.projectUrl || Formio.projectUrl;

    // The baseURL is the same as the projectUrl. This is almost certainly against
    // the Open Source server.
    if (project && this.base === project) {
      this.noProject = true;
      this.projectUrl = this.base;
    }

    // Normalize to an absolute path.
    if ((path.indexOf('http') !== 0) && (path.indexOf('//') !== 0)) {
      path = this.base + path;
    }

    var hostparts = Formio.getUrlParts(path);
    var parts = [];
    var hostName = hostparts[1] + hostparts[2];
    path = hostparts.length > 3 ? hostparts[3] : '';
    var queryparts = path.split('?');
    if (queryparts.length > 1) {
      path = queryparts[0];
      this.query = '?' + queryparts[1];
    }

    // Register a specific path.
    var registerPath = (name, base) => {
      this[name + 'sUrl'] = base + '/' + name;
      var regex = new RegExp('\/' + name + '\/([^/]+)');
      if (path.search(regex) !== -1) {
        parts = path.match(regex);
        this[name + 'Url'] = parts ? (base + parts[0]) : '';
        this[name + 'Id'] = (parts.length > 1) ? parts[1] : '';
        base += parts[0];
      }
      return base;
    };

    // Register an array of items.
    var registerItems = function(items, base, staticBase) {
      for (var i in items) {
        if (items.hasOwnProperty(i)) {
          var item = items[i];
          if (item instanceof Array) {
            registerItems(item, base, true);
          }
          else {
            var newBase = registerPath(item, base);
            base = staticBase ? base : newBase;
          }
        }
      }
    };

    if (!this.projectUrl || (this.projectUrl === this.base)) {
      this.projectUrl = hostName;
    }

    if (!this.noProject) {
      // Determine the projectUrl and projectId
      if ((path.search(/(^|\/)(project)($|\/)/) !== -1)) {
        // Get project id as project/:projectId.
        registerItems(['project'], hostName);
      }
      else if (hostName === this.base) {
        // Get project id as first part of path (subdirectory).
        if (hostparts.length > 3 && path.split('/').length > 1) {
          var pathParts = path.split('/');
          pathParts.shift(); // Throw away the first /.
          this.projectId = pathParts.shift();
          path = '/' + pathParts.join('/');
          this.projectUrl = hostName + '/' + this.projectId;
        }
      }
      else {
        // Get project id from subdomain.
        if (hostparts.length > 2 && (hostparts[2].split('.').length > 2 || hostName.indexOf('localhost') !== -1)) {
          this.projectUrl = hostName;
          this.projectId = hostparts[2].split('.')[0];
        }
      }
      this.projectsUrl = this.projectsUrl || this.base + '/project';
    }

    // Configure Form urls and form ids.
    if ((path.search(/(^|\/)(project|form)($|\/)/) !== -1)) {
      registerItems(['form', ['submission', 'action']], this.projectUrl);
    }
    else {
      var subRegEx = new RegExp('\/(submission|action)($|\/.*)');
      var subs = path.match(subRegEx);
      this.pathType = (subs && (subs.length > 1)) ? subs[1] : '';
      path = path.replace(subRegEx, '');
      path = path.replace(/\/$/, '');
      this.formsUrl = this.projectUrl + '/form';
      this.formUrl = this.projectUrl + path;
      this.formId = path.replace(/^\/+|\/+$/g, '');
      var items = ['submission', 'action'];
      for (var i in items) {
        if (items.hasOwnProperty(i)) {
          var item = items[i];
          this[item + 'sUrl'] = this.projectUrl + path + '/' + item;
          if ((this.pathType === item) && (subs.length > 2) && subs[2]) {
            this[item + 'Id'] = subs[2].replace(/^\/+|\/+$/g, '');
            this[item + 'Url'] = this.projectUrl + path + subs[0];
          }
        }
      }
    }

    // Set the app url if it is not set.
    if (!Formio.projectUrlSet) {
      Formio.projectUrl = this.projectUrl;
    }
  }

  delete(type, opts) {
    var _id = type + 'Id';
    var _url = type + 'Url';
    if (!this[_id]) { Promise.reject('Nothing to delete'); }
    Formio.cache = {};
    return this.makeRequest(type, this[_url], 'delete', null, opts);
  }

  index(type, query, opts) {
    var _url = type + 'Url';
    query = query || '';
    if (query && typeof query === 'object') {
      query = '?' + Formio.serialize(query.params);
    }
    return this.makeRequest(type, this[_url] + query, 'get', null, opts);
  }

  save(type, data, opts) {
    var _id = type + 'Id';
    var _url = type + 'Url';
    var method = (this[_id] || data._id) ? 'put' : 'post';
    var reqUrl = this[_id] ? this[_url] : this[type + 'sUrl'];
    if (!this[_id] && data._id && (method === 'put')) {
      reqUrl += '/' + data._id;
    }
    Formio.cache = {};
    return this.makeRequest(type, reqUrl + this.query, method, data, opts);
  }

  load(type, query, opts) {
    var _id = type + 'Id';
    var _url = type + 'Url';
    if (query && typeof query === 'object') {
      query = Formio.serialize(query.params);
    }
    if (query) {
      query = this.query ? (this.query + '&' + query) : ('?' + query);
    }
    else {
      query = this.query;
    }
    if (!this[_id]) { return Promise.reject('Missing ' + _id); }
    return this.makeRequest(type, this[_url] + query, 'get', null, opts);
  }

  makeRequest(type, url, method, data, opts) {
    method = (method || 'GET').toUpperCase();
    if(!opts || typeof opts !== 'object') {
      opts = {};
    }

    var requestArgs = {
      formio: this,
      type: type,
      url: url,
      method: method,
      data: data,
      opts: opts
    };

    var request = Formio.pluginWait('preRequest', requestArgs)
      .then(function() {
        return Formio.pluginGet('request', requestArgs)
          .then(function(result) {
            if (result === null || result === undefined) {
              return Formio.request(url, method, data, opts.header, opts);
            }
            return result;
          });
      });

    return Formio.pluginAlter('wrapRequestPromise', request, requestArgs);
  }

  loadProject(query, opts) {
    return this.load('project', query, opts);
  }

  saveProject(data, opts) {
    return this.save('project', data, opts);
  }

  deleteProject(opts) {
    return this.delete('project', opts);
  }

  static loadProjects(query, opts) {
    query = query || '';
    if (typeof query === 'object') {
      query = '?' + serialize(query.params);
    }
    return Formio.makeStaticRequest(Formio.baseUrl + '/project' + query);
  }

  loadForm(query, opts) {
    return this.load('form', query, opts);
  }

  saveForm(data, opts) {
    return this.save('form', data, opts);
  }

  deleteForm(opts) {
    return this.delete('form', opts);
  }

  loadForms(query, opts) {
    return this.index('forms', query, opts);
  }

  loadSubmission(query, opts) {
    return this.load('submission', query, opts);
  }

  saveSubmission(data, opts) {
    return this.save('submission', data, opts);
  }

  deleteSubmission(opts) {
    return this.delete('submission', opts);
  }

  loadSubmissions(query, opts) {
    return this.index('submissions', query, opts);
  }

  loadAction(query, opts) {
    return this.load('action', query, opts);
  }

  saveAction(data, opts) {
    return this.save('action', data, opts);
  }

  deleteAction(opts) {
    return this.delete('action', opts);
  }

  loadActions(query, opts) {
    return this.index('actions', query, opts);
  }

  availableActions() {
    return this.makeRequest('availableActions', this.formUrl + '/actions');
  }

  actionInfo(name) {
    return this.makeRequest('actionInfo', this.formUrl + '/actions/' + name);
  }

  uploadFile(storage, file, fileName, dir, progressCallback, url) {
    var requestArgs = {
      provider: storage,
      method: 'upload',
      file: file,
      fileName: fileName,
      dir: dir
    }
    var request = Formio.pluginWait('preRequest', requestArgs)
      .then(function() {
        return Formio.pluginGet('fileRequest', requestArgs)
          .then(function(result) {
            if (storage && (result === null || result === undefined)) {
              if (Formio.providers.storage.hasOwnProperty(storage)) {
                var provider = new Formio.providers.storage[storage](this);
                return provider.uploadFile(file, fileName, dir, progressCallback, url);
              }
              else {
                throw('Storage provider not found');
              }
            }
            return result || {url: ''};
          }.bind(this));
      }.bind(this));

    return Formio.pluginAlter('wrapFileRequestPromise', request, requestArgs);
  }

  downloadFile(file) {
    var requestArgs = {
      method: 'download',
      file: file
    };

    var request = Formio.pluginWait('preRequest', requestArgs)
      .then(function() {
        return Formio.pluginGet('fileRequest', requestArgs)
          .then(function(result) {
            if (file.storage && (result === null || result === undefined)) {
              if (Formio.providers.storage.hasOwnProperty(file.storage)) {
                var provider = new Formio.providers.storage[file.storage](this);
                return provider.downloadFile(file);
              }
              else {
                throw('Storage provider not found');
              }
            }
            return result || {url: ''};
          }.bind(this));
      }.bind(this));

    return Formio.pluginAlter('wrapFileRequestPromise', request, requestArgs);
  }

  static getUrlParts(url) {
    var regex = '^(http[s]?:\\/\\/)';
    if (this.base && url.indexOf(this.base) === 0) {
      regex += '(' + this.base.replace(/^http[s]?:\/\//, '') + ')';
    }
    else {
      regex += '([^/]+)';
    }
    regex += '($|\\/.*)';
    return url.match(new RegExp(regex));
  }

  static serialize(obj) {
    var str = [];
    for(var p in obj)
      if (obj.hasOwnProperty(p)) {
        str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
      }
    return str.join("&");
  }

  static makeStaticRequest(url, method, data, opts) {
    method = (method || 'GET').toUpperCase();
    if(!opts || typeof opts !== 'object') {
      opts = {};
    }
    var requestArgs = {
      url: url,
      method: method,
      data: data
    };

    var request = Formio.pluginWait('preRequest', requestArgs)
      .then(function() {
        return Formio.pluginGet('staticRequest', requestArgs)
          .then(function(result) {
            if (result === null || result === undefined) {
              return Formio.request(url, method, data, opts.header, opts);
            }
            return result;
          });
      });

    return Formio.pluginAlter('wrapStaticRequestPromise', request, requestArgs);
  }

  static request(url, method, data, header, opts) {
    if (!url) {
      return Promise.reject('No url provided');
    }
    method = (method || 'GET').toUpperCase();

    // For reverse compatibility, if they provided the ignoreCache parameter,
    // then change it back to the options format where that is a parameter.
    if (typeof opts === 'boolean') {
      opts = {ignoreCache: opts};
    }
    if(!opts || typeof opts !== 'object') {
      opts = {};
    }

    var cacheKey = btoa(url);

    return new Promise(function(resolve, reject) {
      // Get the cached promise to save multiple loads.
      if (!opts.ignoreCache && method === 'GET' && Formio.cache.hasOwnProperty(cacheKey)) {
        return resolve(Formio.cache[cacheKey]);
      }

      resolve(new Promise(function(resolve, reject) {
        // Set up and fetch request
        var headers = header || new Headers({
            'Accept': 'application/json',
            'Content-type': 'application/json; charset=UTF-8'
          });
        var token = Formio.getToken();
        if (token && !opts.noToken) {
          headers.append('x-jwt-token', token);
        }

        var options = {
          method: method,
          headers: headers,
          mode: 'cors'
        };
        if (data) {
          options.body = JSON.stringify(data);
        }

        resolve(fetch(url, options));
      })
        .catch(function(err) {
          err.message = 'Could not connect to API server (' + err.message + ')';
          err.networkError = true;
          throw err;
        })
        .then(function(response) {
          if (!response.ok) {
            if (response.status === 440) {
              Formio.setToken(null);
              Formio.events.emit('formio.sessionExpired', response.body);
            }
            else if (response.status === 401) {
              Formio.events.emit('formio.unauthorized', response.body);
            }
            // Parse and return the error as a rejected promise to reject this promise
            return (response.headers.get('content-type').indexOf('application/json') !== -1 ?
              response.json() : response.text())
              .then(function(error){
                throw error;
              });
          }

          // Handle fetch results
          var token = response.headers.get('x-jwt-token');
          if (response.status >= 200 && response.status < 300 && token && token !== '') {
            Formio.setToken(token);
          }
          // 204 is no content. Don't try to .json() it.
          if (response.status === 204) {
            return {};
          }
          return (response.headers.get('content-type').indexOf('application/json') !== -1
              ? response.json()
              : response.text()
          ).then(function(result) {
            // Add some content-range metadata to the result here
            var range = response.headers.get('content-range');
            if (range && typeof result === 'object') {
              range = range.split('/');
              if(range[0] !== '*') {
                var skipLimit = range[0].split('-');
                result.skip = Number(skipLimit[0]);
                result.limit = skipLimit[1] - skipLimit[0] + 1;
              }
              result.serverCount = range[1] === '*' ? range[1] : Number(range[1]);
            }

            if (!opts.getHeaders) {
              return result;
            }

            var headers = {};
            response.headers.forEach(function(item, key) {
              headers[key] = item;
            });

            return new Promise(function(resolve, reject) {
              resolve({result: result, headers: headers});
            });
          });
        })
        .catch(function(err) {
          if (err === 'Bad Token') {
            Formio.setToken(null);
            Formio.events.emit('formio.badToken', err);
          }
          if (Formio.cache.hasOwnProperty(cacheKey)) {
            // Remove failed promises from cache
            delete Formio.cache[cacheKey];
          }
          // Propagate error so client can handle accordingly
          throw err;
        }));
    })
      .then(function(result) {
        // Save the cache
        if (method === 'GET') {
          Formio.cache[cacheKey] = Promise.resolve(result);
        }

        // Shallow copy result so modifications don't end up in cache
        if(Array.isArray(result)) {
          var resultCopy = result.map(copy);
          resultCopy.skip = result.skip;
          resultCopy.limit = result.limit;
          resultCopy.serverCount = result.serverCount;
          return resultCopy;
        }
        return copy(result);
      });
  }

  static setToken(token) {
    token = token || '';
    if (token === this.token) { return; }
    this.token = token;
    if (!token) {
      Formio.setUser(null);
      // iOS in private browse mode will throw an error but we can't detect ahead of time that we are in private mode.
      try {
        return localStorage.removeItem('formioToken');
      }
      catch(err) {
        return;
      }
    }
    // iOS in private browse mode will throw an error but we can't detect ahead of time that we are in private mode.
    try {
      localStorage.setItem('formioToken', token);
    }
    catch(err) {
      // Do nothing.
    }
    return Formio.currentUser(); // Run this so user is updated if null
  }

  static getToken() {
    if (this.token) { return this.token; }
    try {
      var token = localStorage.getItem('formioToken') || '';
      this.token = token;
      return token;
    }
    catch (e) {
      return '';
    }
  }

  static setUser(user) {
    if (!user) {
      this.setToken(null);
      // iOS in private browse mode will throw an error but we can't detect ahead of time that we are in private mode.
      try {
        return localStorage.removeItem('formioUser');
      }
      catch(err) {
        return;
      }
    }
    // iOS in private browse mode will throw an error but we can't detect ahead of time that we are in private mode.
    try {
      localStorage.setItem('formioUser', JSON.stringify(user));
    }
    catch(err) {
      // Do nothing.
    }
  }

  static getUser() {
    try {
      return JSON.parse(localStorage.getItem('formioUser') || null);
    }
    catch (e) {
      return;
    }
  }

  static setBaseUrl(url) {
    Formio.baseUrl = url;
    if (!Formio.projectUrlSet) {
      Formio.projectUrl = url;
    }
  }

  static getBaseUrl() {
    return Formio.baseUrl;
  }

  static setApiUrl(url) {
    return Formio.setBaseUrl(url);
  }

  static getApiUrl() {
    return Formio.getBaseUrl();
  }

  static setAppUrl(url) {
    console.warn('Formio.setAppUrl() is deprecated. Use Formio.setProjectUrl instead.')
    Formio.projectUrl = url;
    Formio.projectUrlSet = true;
  }

  static setProjectUrl(url) {
    Formio.projectUrl = url;
    Formio.projectUrlSet = true;
  }

  static getAppUrl() {
    console.warn('Formio.getAppUrl() is deprecated. Use Formio.getProjectUrl instead.')
    return Formio.projectUrl;
  }

  static getProjectUrl() {
    return Formio.projectUrl;
{'fraction_non_alphanumeric': 0.15291509530119254, 'fraction_numerical': 0.0033635715013760063, 'mean_word_length': 2.0879691557164213, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 25, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2793662', 'n_tokens_mistral': 11885, 'n_tokens_neox': 11638, 'n_words': 2816}