import electron from 'electron';
import path from 'path';
import { Curl } from 'insomnia-libcurl';
import fs from 'fs';
import LocalStorage from './local-storage';
import {
  CHANGELOG_BASE_URL,
  MNEMONIC_SYM,
  getAppLongName,
  getAppName,
  getAppVersion,
  isDevelopment,
  isMac,
} from '../common/constants';
import * as misc from '../common/misc';

const { app, Menu, BrowserWindow, shell, dialog } = electron;

const DEFAULT_WIDTH = 1100;
const DEFAULT_HEIGHT = 550;
const MINIMUM_WIDTH = 500;
const MINIMUM_HEIGHT = 400;

let mainWindow = null;
let localStorage = null;

export function init() {
  initLocalStorage();
  initContextMenus();
}

export function createWindow() {
  const zoomFactor = getZoomFactor();
  const { bounds, fullscreen, maximize } = getBounds();
  const { x, y, width, height } = bounds;

  let isVisibleOnAnyDisplay = true;
  for (const d of electron.screen.getAllDisplays()) {
    const isVisibleOnDisplay =
      x >= d.bounds.x &&
      y >= d.bounds.y &&
      x + width <= d.bounds.x + d.bounds.width &&
      y + height <= d.bounds.y + d.bounds.height;

    if (!isVisibleOnDisplay) {
      isVisibleOnAnyDisplay = false;
    }
  }

  mainWindow = new BrowserWindow({
    // Make sure we don't initialize the window outside the bounds
    x: isVisibleOnAnyDisplay ? x : undefined,
    y: isVisibleOnAnyDisplay ? y : undefined,

    // Other options
    fullscreen: fullscreen,
    fullscreenable: true,
    title: getAppName(),
    width: width || DEFAULT_WIDTH,
    height: height || DEFAULT_HEIGHT,
    minHeight: MINIMUM_HEIGHT,
    minWidth: MINIMUM_WIDTH,
    acceptFirstMouse: true,
    icon: path.resolve(__dirname, 'static/icon.png'),
    webPreferences: {
      zoomFactor: zoomFactor,
    },
  });

  // BrowserWindow doesn't have an option for this, so we have to do it manually :(
  if (maximize) {
    mainWindow.maximize();
  }

  mainWindow.on('resize', e => saveBounds());

  mainWindow.on('maximize', e => saveBounds());

  mainWindow.on('unmaximize', e => saveBounds());

  mainWindow.on('move', e => saveBounds());

  mainWindow.on('unresponsive', e => {
    showUnresponsiveModal();
  });

  // Open generic links (<a .../>) in default browser
  mainWindow.webContents.on('will-navigate', (e, url) => {
    if (url === appUrl) {
      return;
    }

    console.log('[app] Navigate to ' + url);
    e.preventDefault();
    electron.shell.openExternal(url);
  });

  // Load the html of the app.
  const url = process.env.APP_RENDER_URL;
  const appUrl = url || `file://${app.getAppPath()}/renderer.html`;
  console.log(`[main] Loading ${appUrl}`);
  mainWindow.loadURL(appUrl);

  // Emitted when the window is closed.
  mainWindow.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null;
  });

  const applicationMenu = {
    label: `${MNEMONIC_SYM}Application`,
    submenu: [
      ...(isMac()
        ? [{ label: `A${MNEMONIC_SYM}bout ${getAppName()}`, role: 'about' }, { type: 'separator' }]
        : []),
      {
        label: `${MNEMONIC_SYM}Preferences`,
        click: function(menuItem, window, e) {
          if (!window || !window.webContents) {
            return;
          }
          window.webContents.send('toggle-preferences');
        },
      },
      {
        label: `${MNEMONIC_SYM}Changelog`,
        click: function(menuItem, window, e) {
          if (!window || !window.webContents) {
            return;
          }
          misc.clickLink(`${CHANGELOG_BASE_URL}/${getAppVersion()}/`);
        },
      },
      { type: 'separator' },
      { role: 'hide' },
      { role: 'hideothers' },
      { type: 'separator' },
      { label: `${MNEMONIC_SYM}Quit`, accelerator: 'CmdOrCtrl+Q', click: () => app.quit() },
    ],
  };

  const editMenu = {
    label: `${MNEMONIC_SYM}Edit`,
    submenu: [
      { label: `${MNEMONIC_SYM}Undo`, accelerator: 'CmdOrCtrl+Z', selector: 'undo:' },
      { label: `${MNEMONIC_SYM}Redo`, accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' },
      { type: 'separator' },
      { label: `Cu${MNEMONIC_SYM}t`, accelerator: 'CmdOrCtrl+X', selector: 'cut:' },
      { label: `${MNEMONIC_SYM}Copy`, accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
      { label: `${MNEMONIC_SYM}Paste`, accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
      {
        label: `Select ${MNEMONIC_SYM}All`,
        accelerator: 'CmdOrCtrl+A',
        selector: 'selectAll:',
      },
    ],
  };

  const viewMenu = {
    label: `${MNEMONIC_SYM}View`,
    submenu: [
      { label: `Toggle ${MNEMONIC_SYM}Full Screen`, role: 'togglefullscreen' },
      {
        label: `${MNEMONIC_SYM}Actual Size`,
        accelerator: 'CmdOrCtrl+0',
        click: () => {
          const w = BrowserWindow.getFocusedWindow();
          if (!w || !w.webContents) {
            return;
          }

          const zoomFactor = 1;
          w.webContents.setZoomFactor(zoomFactor);
          saveZoomFactor(zoomFactor);
        },
      },
      {
        label: `Zoom ${MNEMONIC_SYM}In`,
        accelerator: 'CmdOrCtrl+=',
        click: () => {
          const w = BrowserWindow.getFocusedWindow();
          if (!w || !w.webContents) {
            return;
          }

          const zoomFactor = Math.min(1.8, getZoomFactor() + 0.05);
          w.webContents.setZoomFactor(zoomFactor);

          saveZoomFactor(zoomFactor);
        },
      },
      {
        label: `Zoom ${MNEMONIC_SYM}Out`,
        accelerator: 'CmdOrCtrl+-',
        click: () => {
          const w = BrowserWindow.getFocusedWindow();
          if (!w || !w.webContents) {
            return;
          }

          const zoomFactor = Math.max(0.5, getZoomFactor() - 0.05);
          w.webContents.setZoomFactor(zoomFactor);
          saveZoomFactor(zoomFactor);
        },
      },
      {
        label: 'Toggle Sidebar',
        click: () => {
          const w = BrowserWindow.getFocusedWindow();
          if (!w || !w.webContents) {
            return;
          }

          w.webContents.send('toggle-sidebar');
        },
      },
      {
        label: `Toggle ${MNEMONIC_SYM}DevTools`,
        click: () => mainWindow.toggleDevTools(),
      },
    ],
  };

  const windowMenu = {
    label: `${MNEMONIC_SYM}Window`,
    role: 'window',
    submenu: [
      { label: `${MNEMONIC_SYM}Minimize`, role: 'minimize' },
      ...(isMac() ? [{ label: `${MNEMONIC_SYM}Close`, role: 'close' }] : []),
    ],
  };

  const helpMenu = {
    label: `${MNEMONIC_SYM}Help`,
    role: 'help',
    id: 'help',
    submenu: [
      {
        label: `Contact ${MNEMONIC_SYM}Support`,
        click: () => {
          shell.openExternal('https://insomnia.rest/support/');
        },
      },
      {
        label: `${MNEMONIC_SYM}Keyboard Shortcuts`,
        accelerator: 'CmdOrCtrl+Shift+?',
        click: (menuItem, w, e) => {
          if (!w || !w.webContents) {
            return;
          }
          w.webContents.send('toggle-preferences-shortcuts');
        },
      },
      {
        label: `Show App ${MNEMONIC_SYM}Data Folder`,
        click: (menuItem, w, e) => {
          const directory = misc.getDataDirectory();
          shell.showItemInFolder(directory);
        },
      },
      {
        label: 'Show Open Source Licenses',
        click: (menuItem, w, e) => {
          const licensePath = path.resolve(app.getAppPath(), '../opensource-licenses.txt');
          shell.openItem(licensePath);
        },
      },
      {
        label: `Insomnia ${MNEMONIC_SYM}Help`,
        accelerator: !isMac() ? 'F1' : null,
        click: () => {
          shell.openExternal('https://support.insomnia.rest');
        },
      },
    ],
  };

  if (!isMac()) {
    helpMenu.submenu.unshift({
      label: `${MNEMONIC_SYM}About`,
      click: () => {
        dialog.showMessageBox({
          type: 'info',
          title: getAppName(),
          message: getAppLongName(),
          detail: [
            'Version ' + getAppVersion(),
            'Shell ' + process.versions.electron,
            'Node ' + process.versions.node,
            'V8 ' + process.versions.v8,
            'Architecture ' + process.arch,
            '', // Blank line before libcurl
            Curl.getVersion(),
          ].join('\n'),
        });
      },
    });
  }

  const developerMenu = {
    label: `${MNEMONIC_SYM}Developer`,
    position: 'before=help',
    submenu: [
      {
        label: `${MNEMONIC_SYM}Reload`,
        accelerator: 'Shift+F5',
        click: () => mainWindow.reload(),
      },
      {
        label: `Toggle ${MNEMONIC_SYM}DevTools`,
        accelerator: 'Alt+CmdOrCtrl+I',
        click: () => mainWindow.toggleDevTools(),
      },
      {
        label: `Resize to Defaul${MNEMONIC_SYM}t`,
        click: () =>
          mainWindow.setBounds({
            x: 100,
            y: 100,
            width: DEFAULT_WIDTH,
            height: DEFAULT_HEIGHT,
          }),
      },
      {
        label: `Take ${MNEMONIC_SYM}Screenshot`,
        click: function() {
          mainWindow.capturePage(image => {
            const buffer = image.toPNG();
            const dir = app.getPath('desktop');
            fs.writeFileSync(path.join(dir, `Screenshot-${new Date()}.png`), buffer);
          });
        },
      },
    ],
  };

  const toolsMenu = {
    label: `${MNEMONIC_SYM}Tools`,
    submenu: [
      {
        label: `${MNEMONIC_SYM}Reload Plugins`,
        click: () => {
          const w = BrowserWindow.getFocusedWindow();
          if (!w || !w.webContents) {
            return;
          }

          w.webContents.send('reload-plugins');
        },
      },
    ],
  };

  let template = [];

  template.push(applicationMenu);
  template.push(editMenu);
  template.push(viewMenu);
  template.push(windowMenu);
  template.push(toolsMenu);
  template.push(helpMenu);

  if (isDevelopment() || process.env.INSOMNIA_FORCE_DEBUG) {
    template.push(developerMenu);
  }

  Menu.setApplicationMenu(Menu.buildFromTemplate(template));

  return mainWindow;
}

function showUnresponsiveModal() {
  dialog.showMessageBox(
    {
      type: 'info',
      buttons: ['Cancel', 'Reload'],
      defaultId: 1,
      cancelId: 0,
      title: 'Unresponsive',
      message: 'Insomnia has become unresponsive. Do you want to reload?',
    },
    id => {
      if (id === 1) {
        mainWindow.destroy();
        createWindow();
      }
    },
  );
}

function saveBounds() {
  if (!mainWindow) {
    return;
  }

  const fullscreen = mainWindow.isFullScreen();

  // Only save the size if we're not in fullscreen
  if (!fullscreen) {
    localStorage.setItem('bounds', mainWindow.getBounds());
    localStorage.setItem('maximize', mainWindow.isMaximized());
    localStorage.setItem('fullscreen', false);
  } else {
    localStorage.setItem('fullscreen', true);
  }
}

function getBounds() {
  let bounds = {};
  let fullscreen = false;
  let maximize = false;
  try {
    bounds = localStorage.getItem('bounds', {});
    fullscreen = localStorage.getItem('fullscreen', false);
    maximize = localStorage.getItem('maximize', false);
  } catch (e) {
    // This should never happen, but if it does...!
    console.error('Failed to parse window bounds', e);
  }

  return { bounds, fullscreen, maximize };
}

function saveZoomFactor(zoomFactor) {
  localStorage.setItem('zoomFactor', zoomFactor);
}

function getZoomFactor() {
  let zoomFactor = 1;
  try {
    zoomFactor = localStorage.getItem('zoomFactor', 1);
  } catch (e) {
    // This should never happen, but if it does...!
    console.error('Failed to parse zoomFactor', e);
  }

  return zoomFactor;
}

function initLocalStorage() {
  const localStoragePath = path.join(misc.getDataDirectory(), 'localStorage');
  localStorage = new LocalStorage(localStoragePath);
}

function initContextMenus() {
  require('electron-context-menu')({});
}


keep keep keep replace keep keep keep keep keep

// @flow
import * as electron from 'electron';
import mimes from 'mime-types';
import fs from 'fs';
import path from 'path';
import type { RequestBodyParameter } from '../models/request';

export const DEFAULT_BOUNDARY = 'X-INSOMNIA-BOUNDARY';

export async function buildMultipart(params: Array<RequestBodyParameter>) {
  return new Promise(async (resolve: Function, reject: Function) => {
    const filePath = path.join(electron.remote.app.getPath('temp'), Math.random() + '.body');
    const writeStream = fs.createWriteStream(filePath);
    const lineBreak = '\r\n';
    let totalSize = 0;

    function addFile(path: string): Promise<void> {
      return new Promise((resolve, reject) => {
        let size;
        try {
          size = fs.statSync(path).size;
        } catch (err) {
          reject(err);
        }

        const stream = fs.createReadStream(path);
        stream.once('end', () => {
          resolve();
        });

        stream.once('error', err => {
          reject(err);
        });

        stream.pipe(
          writeStream,
          { end: false },
        );
        totalSize += size;
      });
    }

    const addString = (v: string) => {
      const buffer = Buffer.from(v);
      writeStream.write(buffer);
      totalSize += buffer.length;
    };

    for (const param of params) {
      const noName = !param.name;
      const noValue = !(param.value || param.fileName);

      if (noName && noValue) {
        continue;
      }

      addString(`--${DEFAULT_BOUNDARY}`);
      addString(lineBreak);

      if (param.type === 'file' && param.fileName) {
        const name = param.name || '';
        const fileName = param.fileName;
        const contentType = mimes.lookup(fileName) || 'application/octet-stream';
        addString(
          'Content-Disposition: form-data; ' +
            `name="${name.replace(/"/g, '\\"')}"; ` +
            `filename="${path.basename(fileName).replace(/"/g, '\\"')}"`,
        );
        addString(lineBreak);
        addString(`Content-Type: ${contentType}`);
        addString(lineBreak);
        addString(lineBreak);
        try {
          await addFile(fileName);
        } catch (err) {
          return reject(err);
        }
      } else {
        const name = param.name || '';
        const value = param.value || '';
        const contentType = param.multiline;
        addString(`Content-Disposition: form-data; name="${name}"`);
        addString(lineBreak);
        if (contentType) {
          addString(`Content-Type: ${contentType}`);
          addString(lineBreak);
        }
        addString(lineBreak);
        addString(value);
      }

      addString(lineBreak);
    }

    addString(`--${DEFAULT_BOUNDARY}--`);
    addString(lineBreak);

    writeStream.once('error', err => {
      reject(err);
    });

    writeStream.once('close', () => {
      resolve({
        boundary: DEFAULT_BOUNDARY,
        filePath,
        contentLength: totalSize,
      });
    });

    // We're done here. End the stream and tell FS to save/close the file.
    writeStream.end();
  });
}


const {
  buildQueryStringFromParams,
  joinUrlAndQueryString,
  smartEncodeUrl,
} = require('insomnia-url');
const { jarFromCookies } = require('insomnia-cookies');

module.exports.templateTags = [
  {
    name: 'request',
    displayName: 'Request',
    description: 'reference value from current request',
    args: [
      {
        displayName: 'Attribute',
        type: 'enum',
        options: [
          {
            displayName: 'Name',
            value: 'name',
            description: 'name of request',
          },
          {
            displayName: 'Folder',
            value: 'folder',
            description: 'name of parent folder (or workspace)',
          },
          {
            displayName: 'URL',
            value: 'url',
            description: 'fully qualified URL',
          },
          {
            displayName: 'Query Parameter',
            value: 'parameter',
            description: 'query parameter by name',
          },
          {
            displayName: 'Header',
            value: 'header',
            description: 'header value by name',
          },
          {
            displayName: 'Cookie',
            value: 'cookie',
            description: 'cookie value by name',
          },
          {
            displayName: 'OAuth 2.0 Token',
            value: 'oauth2',
            description: 'access token',
          },
        ],
      },
      {
        type: 'string',
        hide: args => ['url', 'oauth2', 'name', 'folder'].includes(args[0].value),
        displayName: args => {
          switch (args[0].value) {
            case 'cookie':
              return 'Cookie Name';
            case 'parameter':
              return 'Query Parameter Name';
            case 'header':
              return 'Header Name';
            default:
              return 'Name';
          }
        },
      },
      {
        hide: args => args[0].value !== 'folder',
        displayName: 'Parent Index',
        help: 'Specify an index (Starting at 0) for how high up the folder tree to look',
        type: 'number',
      },
    ],

    async run(context, attribute, name, folderIndex) {
      const { meta } = context;

      if (!meta.requestId || !meta.workspaceId) {
        return null;
      }

      const request = await context.util.models.request.getById(meta.requestId);
      const workspace = await context.util.models.workspace.getById(meta.workspaceId);

      if (!request) {
        throw new Error(`Request not found for ${meta.requestId}`);
      }

      if (!workspace) {
        throw new Error(`Workspace not found for ${meta.workspaceId}`);
      }

      switch (attribute) {
        case 'url':
          return getRequestUrl(context, request);
        case 'cookie':
          if (!name) {
            throw new Error('No cookie specified');
          }

          const cookieJar = await context.util.models.cookieJar.getOrCreateForWorkspace(workspace);
          const url = await getRequestUrl(context, request);
          return getCookieValue(cookieJar, url, name);
        case 'parameter':
          if (!name) {
            throw new Error('No query parameter specified');
          }

          const parameterNames = [];

          if (request.parameters.length === 0) {
            throw new Error(`No query parameters available`);
          }

          for (const queryParameter of request.parameters) {
            const queryParameterName = await context.util.render(queryParameter.name);
            parameterNames.push(queryParameterName);
            if (queryParameterName.toLowerCase() === name.toLowerCase()) {
              return context.util.render(queryParameter.value);
            }
          }

          const parameterNamesStr = parameterNames.map(n => `"${n}"`).join(',\n\t');
          throw new Error(
            `No query parameter with name "${name}".\nChoices are [\n\t${parameterNamesStr}\n]`,
          );
        case 'header':
          if (!name) {
            throw new Error('No header specified');
          }

          const headerNames = [];

          if (request.headers.length === 0) {
            throw new Error(`No headers available`);
          }

          for (const header of request.headers) {
            const headerName = await context.util.render(header.name);
            headerNames.push(headerName);
            if (headerName.toLowerCase() === name.toLowerCase()) {
              return context.util.render(header.value);
            }
          }

          const headerNamesStr = headerNames.map(n => `"${n}"`).join(',\n\t');
          throw new Error(`No header with name "${name}".\nChoices are [\n\t${headerNamesStr}\n]`);
        case 'oauth2':
          const token = await context.util.models.oAuth2Token.getByRequestId(request._id);
          if (!token) {
            throw new Error('No OAuth 2.0 tokens found for request');
          }
          return token.accessToken;
        case 'name':
          return request.name;
        case 'folder':
          const ancestors = await context.util.models.request.getAncestors(request);
          const doc = ancestors[folderIndex || 0];
          if (!doc) {
            throw new Error(
              `Could not get folder by index ${folderIndex}. Must be between 0-${ancestors.length -
                1}`,
            );
          }
          return doc ? doc.name : null;
      }

      return null;
    },
  },
];

async function getRequestUrl(context, request) {
  const url = await context.util.render(request.url);
  const parameters = [];
  for (const p of request.parameters) {
    parameters.push({
      name: await context.util.render(p.name),
      value: await context.util.render(p.value),
    });
  }

  const qs = buildQueryStringFromParams(parameters);
  const finalUrl = joinUrlAndQueryString(url, qs);

  return smartEncodeUrl(finalUrl, request.settingEncodeUrl);
}

function getCookieValue(cookieJar, url, name) {
  return new Promise((resolve, reject) => {
    const jar = jarFromCookies(cookieJar.cookies);

    jar.getCookies(url, {}, (err, cookies) => {
      if (err) {
        console.warn(`Failed to find cookie for ${url}`, err);
      }

      if (!cookies || cookies.length === 0) {
        reject(new Error(`No cookies in store for url "${url}"`));
      }

      const cookie = cookies.find(cookie => cookie.key === name);
      if (!cookie) {
        const names = cookies.map(c => `"${c.key}"`).join(',\n\t');
        throw new Error(
          `No cookie with name "${name}".\nChoices are [\n\t${names}\n] for url "${url}"`,
        );
      } else {
        resolve(cookie ? cookie.value : null);
      }
    });
  });
}


import * as sync from '../index';
import * as models from '../../models';
import * as db from '../../common/database';
import * as syncStorage from '../storage';
import * as network from '../network';
import { globalBeforeEach } from '../../__jest__/before-each';
import * as session from '../../account/session';
import * as crypt from '../../account/crypt';

describe('Test push/pull behaviour', () => {
  beforeEach(async () => {
    await globalBeforeEach();

    // Reset some things
    sync._testReset();
    await _setSessionData();
    await _setupSessionMocks();

    // Init sync and storage
    const config = { inMemoryOnly: true, autoload: false, filename: null };
    await syncStorage.initDB(config, true);

    // Add some data
    await models.workspace.create({ _id: 'wrk_1', name: 'Workspace 1' });
    await models.workspace.create({ _id: 'wrk_2', name: 'Workspace 2' });
    await models.request.create({
      _id: 'req_1',
      name: 'Request 1',
      parentId: 'wrk_1',
    });
    await models.request.create({
      _id: 'req_2',
      name: 'Request 2',
      parentId: 'wrk_2',
    });

    // Create resources, resource groups, and configs
    const workspaces = await models.workspace.all();
    const requests = await models.request.all();
    for (const d of [...workspaces, ...requests]) {
      await sync.getOrCreateResourceForDoc(d);
    }
  });

  it('Pushes sync mode with and without resource group id', async () => {
    const request = await models.request.getById('req_1');
    const request2 = await models.request.getById('req_2');
    const resourceRequest = await syncStorage.getResourceByDocId(request._id);
    const resourceRequest2 = await syncStorage.getResourceByDocId(request2._id);

    // Set up sync modes
    await sync.createOrUpdateConfig(resourceRequest.resourceGroupId, {
      syncMode: syncStorage.SYNC_MODE_ON,
    });
    await sync.createOrUpdateConfig(resourceRequest2.resourceGroupId, {
      syncMode: syncStorage.SYNC_MODE_UNSET,
    });

    await sync.push(); // Push only active configs
    await sync.push(resourceRequest.resourceGroupId); // Force push rg_1
    await sync.push(resourceRequest2.resourceGroupId); // Force push rg_2

    expect(network.syncPush.mock.calls.length).toBe(3);
    expect(network.syncPush.mock.calls[0][0].length).toBe(2);
    expect(network.syncPush.mock.calls[0][0][0].id).toBe('wrk_1');
    expect(network.syncPush.mock.calls[0][0][1].id).toBe('req_1');
    expect(network.syncPush.mock.calls[1][0].length).toBe(2);
    expect(network.syncPush.mock.calls[1][0][0].id).toBe('wrk_1');
    expect(network.syncPush.mock.calls[1][0][1].id).toBe('req_1');
    expect(network.syncPush.mock.calls[2][0].length).toBe(2);
    expect(network.syncPush.mock.calls[2][0][0].id).toBe('wrk_2');
    expect(network.syncPush.mock.calls[2][0][1].id).toBe('req_2');
  });

  it('Updates dirty flag for push response', async () => {
    const request = await models.request.getById('req_1');
    const resourceRequest = await syncStorage.getResourceByDocId(request._id);
    await sync.createOrUpdateConfig(resourceRequest.resourceGroupId, {
      syncMode: syncStorage.SYNC_MODE_ON,
    });

    network.syncPush.mockReturnValueOnce({
      updated: [],
      created: [{ id: request._id, version: 'new-version' }],
      removed: [],
      conflicts: [],
    });

    const resourceBefore = await syncStorage.getResourceByDocId(request._id);
    await sync.push(resourceRequest.resourceGroupId);
    const resourceAfter = await syncStorage.getResourceByDocId(request._id);

    expect(network.syncPush.mock.calls.length).toBe(1);
    expect(network.syncPush.mock.calls[0][0].length).toBe(2);
    expect(network.syncPush.mock.calls[0][0][0].id).toBe('wrk_1');
    expect(network.syncPush.mock.calls[0][0][1].id).toBe('req_1');
    expect(resourceBefore.dirty).toBe(true);
    expect(resourceAfter.dirty).toBe(false);
  });

  it('Updates resources for pull response', async () => {
    const request = await models.request.getById('req_1');
    const request2 = await models.request.getById('req_2');
    const requestNew = Object.assign({}, request, {
      _id: 'req_new',
      name: 'New Request',
    });
    const resourceBefore = await syncStorage.getResourceByDocId(request._id);
    const resource2Before = await syncStorage.getResourceByDocId(requestNew._id);
    await sync.createOrUpdateConfig(resourceBefore.resourceGroupId, {
      syncMode: syncStorage.SYNC_MODE_ON,
    });
    const updatedRequest = Object.assign({}, request, {
      name: 'Request Updated',
    });
    const updatedResource = Object.assign({}, resourceBefore, {
      version: 'ver1',
      encContent: await sync.encryptDoc(resourceBefore.resourceGroupId, updatedRequest),
    });
    const createdResourceNew = Object.assign({}, resourceBefore, {
      id: requestNew._id,
      resourceGroupId: 'rg_1',
      encContent: await sync.encryptDoc(resourceBefore.resourceGroupId, requestNew),
    });

    network.syncPull.mockReturnValueOnce({
      updatedResources: [updatedResource],
      createdResources: [createdResourceNew],
      idsToPush: [],
      idsToRemove: ['req_2'],
    });

    // Pull and get docs/resources
    await sync.pull(resourceBefore.resourceGroupId);
    const requestAfter = await models.request.getById(request._id);
    const request2After = await models.request.getById(request2._id);
    const requestNewAfter = await models.request.getById('req_new');
    const resourceAfter = await syncStorage.getResourceByDocId(
      request._id,
      resourceBefore.resourceGroupId,
    );
    const resource2After = await syncStorage.getResourceByDocId(request2._id);
    const resourceNewAfter = await syncStorage.getResourceByDocId(requestNewAfter._id);

    // Assert
    expect(resourceBefore.version).toBe('__NO_VERSION__');
    expect(resourceAfter.version).toBe(updatedResource.version);
    expect(resourceBefore.dirty).toBe(true);
    expect(resource2Before).toBe(null);
    expect(resourceAfter.dirty).toBe(false);
    expect(resource2After.removed).toBe(true);
    expect(requestAfter.name).toBe('Request Updated');
    expect(request2After).toBe(null);
    expect(resourceNewAfter).not.toBe(null);
  });

  it('Conflict: local version wins on modified before', async () => {
    const requestClient = await models.request.getById('req_1');
    const requestServer = Object.assign({}, requestClient, {
      name: 'Server Request',
    });
    const resourceRequest = await syncStorage.getResourceByDocId(requestClient._id);
    const resourceConflict = Object.assign({}, resourceRequest, {
      version: 'ver-2',
      encContent: await sync.encryptDoc(resourceRequest.resourceGroupId, requestServer),
      lastEdited: resourceRequest.lastEdited - 1000, // Same edited time
    });

    network.syncPush.mockReturnValueOnce({
      updated: [],
      created: [],
      removed: [],
      conflicts: [resourceConflict],
    });

    await sync.push(resourceRequest.resourceGroupId);
    const resourceAfter = await syncStorage.getResourceByDocId(
      requestClient._id,
      resourceRequest.resourceGroupId,
    );
    const requestAfter = await models.request.getById(requestClient._id);

    // Assert
    expect(network.syncPush.mock.calls.length).toBe(1);
    expect(network.syncPush.mock.calls[0][0].length).toBe(2);
    // Even when local wins, local resource gets the remove resource version
    expect(resourceAfter.version).toBe(resourceConflict.version);
    // Local resource gets marked as dirty so it's pushed right away
    expect(resourceAfter.dirty).toBe(true);
    // Local db should not be changed since the local won
    expect(requestAfter).toEqual(requestClient);
  });

  it('Conflict: local version wins on modified tie', async () => {
    const requestClient = await models.request.getById('req_1');
    const requestServer = Object.assign({}, requestClient, {
      name: 'Server Request',
    });
    const resourceRequest = await syncStorage.getResourceByDocId(requestClient._id);
    const resourceConflict = Object.assign({}, resourceRequest, {
      version: 'ver-2',
      encContent: await sync.encryptDoc(resourceRequest.resourceGroupId, requestServer),
      lastEdited: resourceRequest.lastEdited, // Same edited time
    });

    network.syncPush.mockReturnValueOnce({
      updated: [],
      created: [],
      removed: [],
      conflicts: [resourceConflict],
    });

    await sync.push(resourceRequest.resourceGroupId);
    const resourceAfter = await syncStorage.getResourceByDocId(
      requestClient._id,
      resourceRequest.resourceGroupId,
    );
    const requestAfter = await models.request.getById(requestClient._id);

    // Assert
    expect(network.syncPush.mock.calls.length).toBe(1);
    expect(network.syncPush.mock.calls[0][0].length).toBe(2);
    // Even when local wins, local resource gets the remove resource version
    expect(resourceAfter.version).toBe(resourceConflict.version);
    // Local resource gets marked as dirty so it's pushed right away
    expect(resourceAfter.dirty).toBe(true);
    // Local db should not be changed since the local won
    expect(requestAfter).toEqual(requestClient);
  });

  it('Conflict: server version wins if modified after', async () => {
    const requestClient = await models.request.getById('req_1');
    const requestServer = Object.assign({}, requestClient, {
      name: 'Server Request',
    });
    const resourceRequest = await syncStorage.getResourceByDocId(requestClient._id);
    const resourceConflict = Object.assign({}, resourceRequest, {
      version: 'ver-2',
      encContent: await sync.encryptDoc(resourceRequest.resourceGroupId, requestServer),
      lastEdited: resourceRequest.lastEdited + 1000,
    });

    network.syncPush.mockReturnValueOnce({
      updated: [],
      created: [],
      removed: [],
      conflicts: [resourceConflict],
    });

    await sync.push(resourceRequest.resourceGroupId);
    const resourceAfter = await syncStorage.getResourceByDocId(
      requestClient._id,
      resourceRequest.resourceGroupId,
    );
    const requestAfter = await models.request.getById(requestClient._id);

    // Assert
    expect(network.syncPush.mock.calls.length).toBe(1);
    expect(network.syncPush.mock.calls[0][0].length).toBe(2);
    expect(resourceAfter.lastEdited).toBeGreaterThan(resourceRequest.lastEdited);
    expect(resourceAfter.version).toBe(resourceConflict.version);
    // Local resource gets marked as dirty so it's pushed right away
    expect(resourceAfter.dirty).toBe(false);
    expect(requestAfter.name).toBe('Server Request');
    expect(requestAfter.modified).toBe(requestServer.modified);
  });
});

describe('Integration tests for creating Resources and pushing', () => {
  beforeEach(async () => {
    await globalBeforeEach();

    // Reset some things
    await _setSessionData();
    sync._testReset();

    // Mock some things
    await _setupSessionMocks();
    jest.useFakeTimers();

    // Init storage
    const config = { inMemoryOnly: true, autoload: false, filename: null };
    await syncStorage.initDB(config, true);

    // Add some data
    await models.workspace.create({
      _id: 'wrk_empty',
      name: 'Workspace Empty',
    });
    await models.workspace.create({ _id: 'wrk_1', name: 'Workspace 1' });
    await models.request.create({
      _id: 'req_1',
      name: 'Request 1',
      parentId: 'wrk_1',
    });
    await models.request.create({
      _id: 'req_2',
      name: 'Request 2',
      parentId: 'wrk_1',
    });
    await models.request.create({
      _id: 'req_3',
      name: 'Request 3',
      parentId: 'wrk_1',
    });
    await models.environment.create({
      _id: 'env_2',
      name: 'Env Prv',
      parentId: 'wrk_1',
      isPrivate: true,
    });

    // Flush changes just to be sure they won't affect our tests
    await db.flushChanges();
    await sync.writePendingChanges();

    // Assert that all our new models were created
    expect((await models.workspace.all()).length).toBe(2);
    expect((await models.request.all()).length).toBe(3);
    expect((await models.environment.all()).length).toBe(1);
    expect((await models.cookieJar.all()).length).toBe(0);

    // Assert that initializing sync will create the initial resources
    expect((await syncStorage.allConfigs()).length).toBe(0);
    expect((await syncStorage.allResources()).length).toBe(0);
    const promise = sync.init();
    jest.runOnlyPendingTimers();
    await promise;
    expect((await syncStorage.allConfigs()).length).toBe(2);
    expect((await syncStorage.allResources()).length).toBe(5);

    // Mark all configs as auto sync
    const configs = await syncStorage.allConfigs();
    for (const config of configs) {
      await syncStorage.updateConfig(config, {
        syncMode: syncStorage.SYNC_MODE_ON,
      });
    }

    // Do initial push
    await sync.push();

    // Reset mocks once again before tests
    await _setupSessionMocks();
  });

  it('Resources created on DB change', async () => {
    // Fetch the workspace and create a new request
    await db.bufferChanges();
    await models.request.create({
      _id: 'req_t',
      url: 'https://google.com',
      parentId: 'wrk_1',
    });

    await db.flushChanges();
    await sync.writePendingChanges();
    await sync.push();

    // Push changes and get resource
    const resource = await syncStorage.getResourceByDocId('req_t');

    // Assert
    expect((await syncStorage.allConfigs()).length).toBe(2);
    expect((await syncStorage.allResources()).length).toBe(6);
    expect(_decryptResource(resource).url).toBe('https://google.com');
    expect(resource.removed).toBe(false);

    expect(network.syncPush.mock.calls.length).toBe(1);
    expect(network.syncPush.mock.calls[0][0].length).toBe(6);

    expect(network.syncPull.mock.calls).toEqual([]);
  });

  it('Resources revived on DB change', async () => {
    // Fetch the workspace and create a new request
    await db.bufferChanges();
    const request = await models.request.create({
      _id: 'req_t',
      name: 'Original Request',
      parentId: 'wrk_1',
    });
    await db.flushChanges();
    await sync.writePendingChanges();
    await sync.push();

    // Mark resource as removed
    const originalResource = await syncStorage.getResourceByDocId('req_t');
    const updatedResource = await syncStorage.updateResource(originalResource, {
      removed: true,
    });

    // Update it and push it again
    await db.bufferChanges();
    await models.request.update(request, { name: 'New Name' });
    await db.flushChanges();
    await sync.writePendingChanges();
    await sync.push();
    const finalResource = await syncStorage.getResourceByDocId('req_t');

    // Assert
    expect(originalResource.removed).toBe(false);
    expect(updatedResource.removed).toBe(true);
    expect(finalResource.removed).toBe(false);
  });

  it('Resources update on DB change', async () => {
    // Create, update a request, and fetch it's resource
    const request = await models.request.getById('req_1');
    const resource = await syncStorage.getResourceByDocId(request._id);
    await db.bufferChanges();
    const updatedRequest = await models.request.update(request, {
      name: 'New Name',
    });

    // Drain and fetch new resource
    await db.flushChanges();
    await sync.writePendingChanges();
    await sync.push();
    const updatedResource = await syncStorage.getResourceByDocId(request._id);

    // Assert
    expect(request.name).toBe('Request 1');
    expect(_decryptResource(resource).name).toBe('Request 1');
    expect(updatedRequest.name).toBe('New Name');
    expect(_decryptResource(updatedResource).name).toBe('New Name');
    expect(resource.removed).toBe(false);

    expect(network.syncPush.mock.calls.length).toBe(1);
    expect(network.syncPush.mock.calls[0][0].length).toBe(5);

    expect(network.syncPull.mock.calls).toEqual([]);
  });

  it('Resources removed on DB change', async () => {
    // Create, update a request, and fetch it's resource
    const request = await models.request.getById('req_1');
    const resource = await syncStorage.getResourceByDocId(request._id);
    await db.bufferChanges();
    await models.request.remove(request);

    // Drain and fetch new resource
    await db.flushChanges();
    await sync.writePendingChanges();
    await sync.push();
    const updatedResource = await syncStorage.getResourceByDocId(request._id);

    // Assert
    expect(resource.removed).toBe(false);
    expect(updatedResource.removed).toBe(true);

    expect(network.syncPush.mock.calls.length).toBe(1);
    expect(network.syncPush.mock.calls[0][0].length).toBe(5);

    expect(network.syncPull.mock.calls).toEqual([]);
  });
});

// ~~~~~~~ //
// Helpers //
// ~~~~~~~ //

function _decryptResource(resource) {
  const message = JSON.parse(resource.encContent);
  const fakeKey = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
  const docJSON = crypt.decryptAES(fakeKey, message);
  return JSON.parse(docJSON);
}

async function _setSessionData() {
  const symmetricKey = {
    alg: 'A256GCM',
    ext: true,
    k: '3-QU2OcQcpSyFIoL8idgclbImP3M8Y2d0oVAca3Vl4g',
    key_ops: ['encrypt', 'decrypt'],
    kty: 'oct',
  };

  const publicKey = {
    alg: 'RSA-OAEP-256',
    e: 'AQAB',
    ext: true,
    key_ops: ['encrypt'],
    kty: 'RSA',
    n: 'aaaa',
  };

  const { privateKey } = await crypt.generateKeyPairJWK();
  const encPrivateKey = {
    ad: '',
    d: Buffer.from(JSON.stringify(privateKey)).toString('hex'),
    iv: '968f1d810efdaec58f9e313e',
    t: '0e87a2e57a198ca79cb99585fe9c244a',
  };

  // Setup mocks and stuff
  session.setSessionData(
    'ses_123',
    'acct_123',
    'Tammy',
    'Tester',
    'greg.schier@konghq.com',
    symmetricKey,
    publicKey,
    encPrivateKey,
  );
}

async function _setupSessionMocks() {
  const resourceGroups = {};

  network.syncCreateResourceGroup = jest.fn((parentId, name, _) => {
    const id = `rg_${Object.keys(resourceGroups).length + 1}`;

    // Generate a public key and use a symmetric equal to it's Id for
    // convenience
    const publicKey = session.getPublicKey();
    const symmetricKeyStr = JSON.stringify({ k: id });
    const encSymmetricKey = crypt.encryptRSAWithJWK(publicKey, symmetricKeyStr);

    // Store the resource group and return it
    resourceGroups[id] = Object.assign(
      {},
      { id, encSymmetricKey },
      {
        parentResourceId: parentId,
        name: name,
        encSymmetricKey: encSymmetricKey,
      },
    );
    return resourceGroups[id];
  });

  network.syncGetResourceGroup = jest.fn(id => {
    if (resourceGroups[id]) {
      return resourceGroups[id];
    }

    const err = new Error(`Not Found for ${id}`);
    err.statusCode = 404;
    throw err;
  });

  network.syncPull = jest.fn(body => ({
    updatedResources: [],
    createdResources: [],
    idsToPush: [],
    idsToRemove: [],
  }));

  network.syncPush = jest.fn(body => ({
    conflicts: [],
    updated: [],
    created: [],
    removed: [],
  }));
}


headers={[...part.headers, ...part.headers, ...part.headers, ...part.headers]}

const { parts, activePart } = this.state;

keep keep keep replace replace replace replace replace keep keep keep keep keep

body: <ResponseHeadersViewer headers={[...part.headers]} />,

import fs from 'fs';
import { globalBeforeEach } from '../../__jest__/before-each';
import { buildMultipart, DEFAULT_BOUNDARY } from '../multipart';
import path from 'path';

describe('buildMultipart()', () => {
  beforeEach(globalBeforeEach);

  it('builds a simple request', async () => {
    const { filePath, boundary, contentLength } = await buildMultipart([
      { name: 'foo', value: 'bar' },
      { name: 'multi-line', value: 'Hello\nWorld!' },
    ]);

    expect(boundary).toBe(DEFAULT_BOUNDARY);
    expect(contentLength).toBe(189);
    expect(fs.readFileSync(filePath, 'utf8')).toBe(
      [
        `--${boundary}`,
        'Content-Disposition: form-data; name="foo"',
        '',
        'bar',
        `--${boundary}`,
        'Content-Disposition: form-data; name="multi-line"',
        '',
        'Hello\nWorld!',
        `--${boundary}--`,
        '',
      ].join('\r\n'),
    );
  });

  it('builds a multiline request with content-type', async () => {
    const { filePath, boundary, contentLength } = await buildMultipart([
      { name: 'foo', value: 'bar' },
      { name: 'json', value: '{"hello": "world"}', multiline: 'application/json' },
    ]);

    expect(boundary).toBe(DEFAULT_BOUNDARY);
    expect(contentLength).toBe(221);
    expect(fs.readFileSync(filePath, 'utf8')).toBe(
      [
        `--${boundary}`,
        'Content-Disposition: form-data; name="foo"',
        '',
        'bar',
        `--${boundary}`,
        'Content-Disposition: form-data; name="json"',
        'Content-Type: application/json',
        '',
        '{"hello": "world"}',
        `--${boundary}--`,
        '',
      ].join('\r\n'),
    );
  });

  it('builds with file', async () => {
    const fileName = path.resolve(path.join(__dirname, './testfile.txt'));
    const { filePath, boundary, contentLength } = await buildMultipart([
      { name: 'foo', value: 'bar' },
      { name: 'file', type: 'file', fileName: fileName },
      { name: 'baz', value: 'qux' },
    ]);

    expect(boundary).toBe(DEFAULT_BOUNDARY);
    expect(contentLength).toBe(322);
    expect(fs.readFileSync(filePath, 'utf8')).toBe(
      [
        `--${boundary}`,
        'Content-Disposition: form-data; name="foo"',
        '',
        'bar',
        `--${boundary}`,
        'Content-Disposition: form-data; name="file"; filename="testfile.txt"',
        'Content-Type: text/plain',
        '',
        'Hello World!\n\nHow are you?',
        `--${boundary}`,
        'Content-Disposition: form-data; name="baz"',
        '',
        'qux',
        `--${boundary}--`,
        '',
      ].join('\r\n'),
    );
  });

  it('skips entries with no name or value', async () => {
    const { filePath, boundary, contentLength } = await buildMultipart([
      { value: 'bar' },
      { name: 'foo' },
      { name: '', value: '' },
      { name: '', type: 'file', fileName: '' },
    ]);

    expect(boundary).toBe(DEFAULT_BOUNDARY);
    expect(contentLength).toBe(167);
    expect(fs.readFileSync(filePath, 'utf8')).toBe(
      [
        `--${boundary}`,
        'Content-Disposition: form-data; name=""',
        '',
        'bar',
        `--${boundary}`,
        'Content-Disposition: form-data; name="foo"',
        '',
        '',
        `--${boundary}--`,
        '',
      ].join('\r\n'),
    );
  });
});


import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import autobind from 'autobind-decorator';
import fs from 'fs';
import { clipboard, ipcRenderer, remote } from 'electron';
import { parse as urlParse } from 'url';
import HTTPSnippet from 'httpsnippet';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Wrapper from '../components/wrapper';
import WorkspaceEnvironmentsEditModal from '../components/modals/workspace-environments-edit-modal';
import Toast from '../components/toast';
import CookiesModal from '../components/modals/cookies-modal';
import RequestSwitcherModal from '../components/modals/request-switcher-modal';
import SettingsModal, { TAB_INDEX_SHORTCUTS } from '../components/modals/settings-modal';
import {
  COLLAPSE_SIDEBAR_REMS,
  DEFAULT_PANE_HEIGHT,
  DEFAULT_PANE_WIDTH,
  DEFAULT_SIDEBAR_WIDTH,
  MAX_PANE_HEIGHT,
  MAX_PANE_WIDTH,
  MAX_SIDEBAR_REMS,
  MIN_PANE_HEIGHT,
  MIN_PANE_WIDTH,
  MIN_SIDEBAR_REMS,
  PREVIEW_MODE_SOURCE,
} from '../../common/constants';
import * as globalActions from '../redux/modules/global';
import * as db from '../../common/database';
import * as models from '../../models';
import {
  selectActiveCookieJar,
  selectActiveOAuth2Token,
  selectActiveRequest,
  selectActiveRequestMeta,
  selectActiveRequestResponses,
  selectActiveResponse,
  selectActiveWorkspace,
  selectActiveWorkspaceClientCertificates,
  selectActiveWorkspaceMeta,
  selectEntitiesLists,
  selectSidebarChildren,
  selectSyncItems,
  selectUnseenWorkspaces,
  selectWorkspaceRequestsAndRequestGroups,
} from '../redux/selectors';
import RequestCreateModal from '../components/modals/request-create-modal';
import GenerateCodeModal from '../components/modals/generate-code-modal';
import WorkspaceSettingsModal from '../components/modals/workspace-settings-modal';
import RequestSettingsModal from '../components/modals/request-settings-modal';
import RequestRenderErrorModal from '../components/modals/request-render-error-modal';
import * as network from '../../network/network';
import { debounce, getContentDispositionHeader, getDataDirectory } from '../../common/misc';
import * as mime from 'mime-types';
import * as path from 'path';
import * as render from '../../common/render';
import { getKeys } from '../../templating/utils';
import { showAlert, showModal, showPrompt } from '../components/modals/index';
import { exportHarRequest } from '../../common/har';
import { hotKeyRefs } from '../../common/hotkeys';
import { executeHotKey } from '../../common/hotkeys-listener';
import KeydownBinder from '../components/keydown-binder';
import ErrorBoundary from '../components/error-boundary';
import * as plugins from '../../plugins';
import * as templating from '../../templating/index';
import AskModal from '../components/modals/ask-modal';
import { updateMimeType } from '../../models/request';
import MoveRequestGroupModal from '../components/modals/move-request-group-modal';
import * as themes from '../../plugins/misc';
import ExportRequestsModal from '../components/modals/export-requests-modal';
import FileSystemDriver from '../../sync/store/drivers/file-system-driver';
import VCS from '../../sync/vcs';
import SyncMergeModal from '../components/modals/sync-merge-modal';

@autobind
class App extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      showDragOverlay: false,
      draggingSidebar: false,
      draggingPaneHorizontal: false,
      draggingPaneVertical: false,
      sidebarWidth: props.sidebarWidth || DEFAULT_SIDEBAR_WIDTH,
      paneWidth: props.paneWidth || DEFAULT_PANE_WIDTH,
      paneHeight: props.paneHeight || DEFAULT_PANE_HEIGHT,
      isVariableUncovered: props.isVariableUncovered || false,
      vcs: null,
      forceRefreshCounter: 0,
      forceRefreshHeaderCounter: 0,
      isMigratingChildren: false,
    };

    this._getRenderContextPromiseCache = {};

    this._savePaneWidth = debounce(paneWidth => this._updateActiveWorkspaceMeta({ paneWidth }));
    this._savePaneHeight = debounce(paneHeight => this._updateActiveWorkspaceMeta({ paneHeight }));
    this._saveSidebarWidth = debounce(sidebarWidth =>
      this._updateActiveWorkspaceMeta({ sidebarWidth }),
    );

    this._globalKeyMap = null;
  }

  _setGlobalKeyMap() {
    this._globalKeyMap = [
      [
        hotKeyRefs.PREFERENCES_SHOW_GENERAL,
        () => {
          showModal(SettingsModal);
        },
      ],
      [
        hotKeyRefs.PREFERENCES_SHOW_KEYBOARD_SHORTCUTS,
        () => {
          showModal(SettingsModal, TAB_INDEX_SHORTCUTS);
        },
      ],
      [
        hotKeyRefs.SHOW_RECENT_REQUESTS,
        () => {
          showModal(RequestSwitcherModal, {
            disableInput: true,
            maxRequests: 10,
            maxWorkspaces: 0,
            selectOnKeyup: true,
            title: 'Recent Requests',
            hideNeverActiveRequests: true,

            // Add an open delay so the dialog won't show for quick presses
            openDelay: 150,
          });
        },
      ],
      [
        hotKeyRefs.WORKSPACE_SHOW_SETTINGS,
        () => {
          const { activeWorkspace } = this.props;
          showModal(WorkspaceSettingsModal, activeWorkspace);
        },
      ],
      [
        hotKeyRefs.REQUEST_SHOW_SETTINGS,
        () => {
          if (this.props.activeRequest) {
            showModal(RequestSettingsModal, {
              request: this.props.activeRequest,
            });
          }
        },
      ],
      [
        hotKeyRefs.REQUEST_QUICK_SWITCH,
        () => {
          showModal(RequestSwitcherModal);
        },
      ],
      [hotKeyRefs.REQUEST_SEND, this._handleSendShortcut],
      [
        hotKeyRefs.ENVIRONMENT_SHOW_EDITOR,
        () => {
          const { activeWorkspace } = this.props;
          showModal(WorkspaceEnvironmentsEditModal, activeWorkspace);
        },
      ],
      [
        hotKeyRefs.SHOW_COOKIES_EDITOR,
        () => {
          const { activeWorkspace } = this.props;
          showModal(CookiesModal, activeWorkspace);
        },
      ],
      [
        hotKeyRefs.REQUEST_QUICK_CREATE,
        async () => {
          const { activeRequest, activeWorkspace } = this.props;
          const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id;
          const request = await models.request.create({ parentId, name: 'New Request' });
          await this._handleSetActiveRequest(request._id);
        },
      ],
      [
        hotKeyRefs.REQUEST_SHOW_CREATE,
        () => {
          const { activeRequest, activeWorkspace } = this.props;
          const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id;
          this._requestCreate(parentId);
        },
      ],
      [
        hotKeyRefs.REQUEST_SHOW_DELETE,
        () => {
          const { activeRequest } = this.props;

          if (!activeRequest) {
            return;
          }

          showModal(AskModal, {
            title: 'Delete Request?',
            message: `Really delete ${activeRequest.name}?`,
            onDone: confirmed => {
              if (!confirmed) {
                return;
              }
              models.request.remove(activeRequest);
            },
          });
        },
      ],
      [
        hotKeyRefs.REQUEST_SHOW_CREATE_FOLDER,
        () => {
          const { activeRequest, activeWorkspace } = this.props;
          const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id;
          this._requestGroupCreate(parentId);
        },
      ],
      [
        hotKeyRefs.REQUEST_SHOW_GENERATE_CODE_EDITOR,
        async () => {
          showModal(GenerateCodeModal, this.props.activeRequest);
        },
      ],
      [
        hotKeyRefs.REQUEST_SHOW_DUPLICATE,
        async () => {
          await this._requestDuplicate(this.props.activeRequest);
        },
      ],
      [
        hotKeyRefs.REQUEST_TOGGLE_PIN,
        async () => {
          if (!this.props.activeRequest) {
            return;
          }

          const metas = Object.values(this.props.entities.requestMetas).find(
            m => m.parentId === this.props.activeRequest._id,
          );

          await this._handleSetRequestPinned(this.props.activeRequest, !(metas && metas.pinned));
        },
      ],
      [hotKeyRefs.PLUGIN_RELOAD, this._handleReloadPlugins],
      [
        hotKeyRefs.ENVIRONMENT_UNCOVER_VARIABLES,
        async () => {
          await this._updateIsVariableUncovered();
        },
      ],
      [
        hotKeyRefs.SIDEBAR_TOGGLE,
        () => {
          this._handleToggleSidebar();
        },
      ],
    ];
  }

  async _handleSendShortcut() {
    const { activeRequest, activeEnvironment } = this.props;
    await this._handleSendRequestWithEnvironment(
      activeRequest ? activeRequest._id : 'n/a',
      activeEnvironment ? activeEnvironment._id : 'n/a',
    );
  }

  _setRequestPaneRef(n) {
    this._requestPane = n;
  }

  _setResponsePaneRef(n) {
    this._responsePane = n;
  }

  _setSidebarRef(n) {
    this._sidebar = n;
  }

  _requestGroupCreate(parentId) {
    showPrompt({
      title: 'New Folder',
      defaultValue: 'My Folder',
      submitName: 'Create',
      label: 'Name',
      selectText: true,
      onComplete: async name => {
        const requestGroup = await models.requestGroup.create({
          parentId,
          name,
        });
        await models.requestGroupMeta.create({
          parentId: requestGroup._id,
          collapsed: false,
        });
      },
    });
  }

  _requestCreate(parentId) {
    showModal(RequestCreateModal, {
      parentId,
      onComplete: request => {
        this._handleSetActiveRequest(request._id);
      },
    });
  }

  static async _requestGroupDuplicate(requestGroup) {
    showPrompt({
      title: 'Duplicate Folder',
      defaultValue: requestGroup.name,
      submitName: 'Create',
      label: 'New Name',
      selectText: true,
      onComplete: async name => {
        await models.requestGroup.duplicate(requestGroup, { name });
      },
    });
  }

  static async _requestGroupMove(requestGroup) {
    showModal(MoveRequestGroupModal, { requestGroup });
  }

  _requestDuplicate(request) {
    if (!request) {
      return;
    }

    showPrompt({
      title: 'Duplicate Request',
      defaultValue: request.name,
      submitName: 'Create',
      label: 'New Name',
      selectText: true,
      onComplete: async name => {
        const newRequest = await models.request.duplicate(request, { name });
        await this._handleSetActiveRequest(newRequest._id);
      },
    });
  }

  _workspaceDuplicate(callback) {
    const workspace = this.props.activeWorkspace;
    showPrompt({
      title: 'Duplicate Workspace',
      defaultValue: workspace.name,
      submitName: 'Create',
      selectText: true,
      label: 'New Name',
      onComplete: async name => {
        const newWorkspace = await db.duplicate(workspace, { name });
        await this.props.handleSetActiveWorkspace(newWorkspace._id);
        callback();
      },
    });
  }

  async _fetchRenderContext() {
    const { activeEnvironment, activeRequest, activeWorkspace } = this.props;
    const environmentId = activeEnvironment ? activeEnvironment._id : null;

    const ancestors = await db.withAncestors(activeRequest || activeWorkspace, [
      models.request.type,
      models.requestGroup.type,
      models.workspace.type,
    ]);

    return render.getRenderContext(activeRequest, environmentId, ancestors);
  }

  async _handleGetRenderContext() {
    const context = await this._fetchRenderContext();
    const keys = getKeys(context);
    return { context, keys };
  }

  /**
   * Heavily optimized render function
   *
   * @param text - template to render
   * @param contextCacheKey - if rendering multiple times in parallel, set this
   * @returns {Promise}
   * @private
   */
  async _handleRenderText(text, contextCacheKey = null) {
    if (!contextCacheKey || !this._getRenderContextPromiseCache[contextCacheKey]) {
      // NOTE: We're caching promises here to avoid race conditions
      this._getRenderContextPromiseCache[contextCacheKey] = this._fetchRenderContext();
    }

    // Set timeout to delete the key eventually
    setTimeout(() => delete this._getRenderContextPromiseCache[contextCacheKey], 5000);

    const context = await this._getRenderContextPromiseCache[contextCacheKey];
    return render.render(text, context);
  }

  _handleGenerateCodeForActiveRequest() {
    App._handleGenerateCode(this.props.activeRequest);
  }

  static _handleGenerateCode(request) {
    showModal(GenerateCodeModal, request);
  }

  async _handleCopyAsCurl(request) {
    const { activeEnvironment } = this.props;
    const environmentId = activeEnvironment ? activeEnvironment._id : 'n/a';
    const har = await exportHarRequest(request._id, environmentId);
    const snippet = new HTTPSnippet(har);
    const cmd = snippet.convert('shell', 'curl');
    clipboard.writeText(cmd);
  }

  static async _updateRequestGroupMetaByParentId(requestGroupId, patch) {
    const requestGroupMeta = await models.requestGroupMeta.getByParentId(requestGroupId);
    if (requestGroupMeta) {
      await models.requestGroupMeta.update(requestGroupMeta, patch);
    } else {
      const newPatch = Object.assign({ parentId: requestGroupId }, patch);
      await models.requestGroupMeta.create(newPatch);
    }
  }

  async _updateActiveWorkspaceMeta(patch) {
    const workspaceId = this.props.activeWorkspace._id;
    const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspaceId);
    if (workspaceMeta) {
      return models.workspaceMeta.update(workspaceMeta, patch);
    } else {
      const newPatch = Object.assign({ parentId: workspaceId }, patch);
      return models.workspaceMeta.create(newPatch);
    }
  }

  static async _updateRequestMetaByParentId(requestId, patch) {
    const requestMeta = await models.requestMeta.getByParentId(requestId);
    if (requestMeta) {
      return models.requestMeta.update(requestMeta, patch);
    } else {
      const newPatch = Object.assign({ parentId: requestId }, patch);
      return models.requestMeta.create(newPatch);
    }
  }

  _updateIsVariableUncovered() {
    this.setState({ isVariableUncovered: !this.state.isVariableUncovered });
  }

  _handleSetPaneWidth(paneWidth) {
    this.setState({ paneWidth });
    this._savePaneWidth(paneWidth);
  }

  _handleSetPaneHeight(paneHeight) {
    this.setState({ paneHeight });
    this._savePaneHeight(paneHeight);
  }

  async _handleSetActiveRequest(activeRequestId) {
    await this._updateActiveWorkspaceMeta({ activeRequestId });
    await App._updateRequestMetaByParentId(activeRequestId, { lastActive: Date.now() });
  }

  async _handleSetActiveEnvironment(activeEnvironmentId) {
    await this._updateActiveWorkspaceMeta({ activeEnvironmentId });

    // Give it time to update and re-render
    setTimeout(() => {
      this._wrapper && this._wrapper._forceRequestPaneRefresh();
    }, 300);
  }

  _handleSetSidebarWidth(sidebarWidth) {
    this.setState({ sidebarWidth });
    this._saveSidebarWidth(sidebarWidth);
  }

  async _handleSetSidebarHidden(sidebarHidden) {
    await this._updateActiveWorkspaceMeta({ sidebarHidden });
  }

  async _handleSetSidebarFilter(sidebarFilter) {
    await this._updateActiveWorkspaceMeta({ sidebarFilter });
  }

  _handleSetRequestGroupCollapsed(requestGroupId, collapsed) {
    App._updateRequestGroupMetaByParentId(requestGroupId, { collapsed });
  }

  async _handleSetRequestPinned(request, pinned) {
    App._updateRequestMetaByParentId(request._id, { pinned });
  }

  _handleSetResponsePreviewMode(requestId, previewMode) {
    App._updateRequestMetaByParentId(requestId, { previewMode });
  }

  _handleUpdateDownloadPath(requestId, downloadPath) {
    App._updateRequestMetaByParentId(requestId, { downloadPath });
  }

  async _handleSetResponseFilter(requestId, responseFilter) {
    await App._updateRequestMetaByParentId(requestId, { responseFilter });

    clearTimeout(this._responseFilterHistorySaveTimeout);
    this._responseFilterHistorySaveTimeout = setTimeout(async () => {
      const meta = await models.requestMeta.getByParentId(requestId);
      const responseFilterHistory = meta.responseFilterHistory.slice(0, 10);

      // Already in history?
      if (responseFilterHistory.includes(responseFilter)) {
        return;
      }

      // Blank?
      if (!responseFilter) {
        return;
      }

      responseFilterHistory.unshift(responseFilter);
      await App._updateRequestMetaByParentId(requestId, {
        responseFilterHistory,
      });
    }, 2000);
  }

  async _handleUpdateRequestMimeType(mimeType) {
    if (!this.props.activeRequest) {
      console.warn('Tried to update request mime-type when no active request');
      return null;
    }

    const requestMeta = await models.requestMeta.getOrCreateByParentId(
      this.props.activeRequest._id,
    );
    const savedBody = requestMeta.savedRequestBody;

    const saveValue =
      typeof mimeType !== 'string' // Switched to No body
        ? this.props.activeRequest.body
        : {}; // Clear saved value in requestMeta

    await models.requestMeta.update(requestMeta, {
      savedRequestBody: saveValue,
    });

    const newRequest = await updateMimeType(this.props.activeRequest, mimeType, false, savedBody);

    // Force it to update, because other editor components (header editor)
    // needs to change. Need to wait a delay so the next render can finish
    setTimeout(() => {
      this.setState({ forceRefreshHeaderCounter: this.state.forceRefreshHeaderCounter + 1 });
    }, 500);

    return newRequest;
  }

  async _getDownloadLocation() {
    return new Promise(resolve => {
      const options = {
        title: 'Select Download Location',
        buttonLabel: 'Send and Save',
        defaultPath: window.localStorage.getItem('insomnia.sendAndDownloadLocation'),
      };

      remote.dialog.showSaveDialog(options, filename => {
        window.localStorage.setItem('insomnia.sendAndDownloadLocation', filename);
        resolve(filename);
      });
    });
  }

  async _handleSendAndDownloadRequestWithEnvironment(requestId, environmentId, dir) {
    const { settings, handleStartLoading, handleStopLoading } = this.props;

    const request = await models.request.getById(requestId);
    if (!request) {
      return;
    }

    // NOTE: Since request is by far the most popular event, we will throttle
    // it so that we only track it if the request has changed since the last one
    const key = request._id;
    if (this._sendRequestTrackingKey !== key) {
      this._sendRequestTrackingKey = key;
    }

    // Start loading
    handleStartLoading(requestId);

    try {
      const responsePatch = await network.send(requestId, environmentId);
      const headers = responsePatch.headers || [];
      const header = getContentDispositionHeader(headers);
      const nameFromHeader = header ? header.value : null;

      if (
        responsePatch.bodyPath &&
        responsePatch.statusCode >= 200 &&
        responsePatch.statusCode < 300
      ) {
        const extension = mime.extension(responsePatch.contentType) || 'unknown';
        const name =
          nameFromHeader || `${request.name.replace(/\s/g, '-').toLowerCase()}.${extension}`;

        let filename;
        if (dir) {
          filename = path.join(dir, name);
        } else {
          filename = await this._getDownloadLocation();
        }

        const to = fs.createWriteStream(filename);
        const readStream = models.response.getBodyStream(responsePatch);

        if (!readStream) {
          return;
        }

        readStream.pipe(to);

        readStream.on('end', async () => {
          responsePatch.error = `Saved to ${filename}`;
          await models.response.create(responsePatch, settings.maxHistoryResponses);
        });

        readStream.on('error', async err => {
          console.warn('Failed to download request after sending', responsePatch.bodyPath, err);
          await models.response.create(responsePatch, settings.maxHistoryResponses);
        });
      } else {
        // Save the bad responses so failures are shown still
        await models.response.create(responsePatch, settings.maxHistoryResponses);
      }
    } catch (err) {
      showAlert({
        title: 'Unexpected Request Failure',
        message: (
          <div>
            <p>The request failed due to an unhandled error:</p>
            <code className="wide selectable">
              <pre>{err.message}</pre>
            </code>
          </div>
        ),
      });
    }

    // Unset active response because we just made a new one
    await App._updateRequestMetaByParentId(requestId, {
      activeResponseId: null,
    });

    // Stop loading
    handleStopLoading(requestId);
  }

  async _handleSendRequestWithEnvironment(requestId, environmentId) {
    const { handleStartLoading, handleStopLoading, settings } = this.props;
    const request = await models.request.getById(requestId);
    if (!request) {
      return;
    }

    // NOTE: Since request is by far the most popular event, we will throttle
    // it so that we only track it if the request has changed since the last noe
    const key = `${request._id}::${request.modified}`;
    if (this._sendRequestTrackingKey !== key) {
      this._sendRequestTrackingKey = key;
    }

    handleStartLoading(requestId);

    try {
      const responsePatch = await network.send(requestId, environmentId);
      await models.response.create(responsePatch, settings.maxHistoryResponses);
    } catch (err) {
      if (err.type === 'render') {
        showModal(RequestRenderErrorModal, { request, error: err });
      } else {
        showAlert({
          title: 'Unexpected Request Failure',
          message: (
            <div>
              <p>The request failed due to an unhandled error:</p>
              <code className="wide selectable">
                <pre>{err.message}</pre>
              </code>
            </div>
          ),
        });
      }
    }

    // Unset active response because we just made a new one
    await App._updateRequestMetaByParentId(requestId, {
      activeResponseId: null,
    });

    // Stop loading
    handleStopLoading(requestId);
  }

  async _handleSetActiveResponse(requestId, activeResponse = null) {
    const { activeEnvironment } = this.props;
    const activeResponseId = activeResponse ? activeResponse._id : null;
    await App._updateRequestMetaByParentId(requestId, { activeResponseId });

    let response;
    if (activeResponseId) {
      response = await models.response.getById(activeResponseId);
    } else {
      const environmentId = activeEnvironment ? activeEnvironment._id : null;
      response = await models.response.getLatestForRequest(requestId, environmentId);
    }

    const requestVersionId = response ? response.requestVersionId : 'n/a';
    const request = await models.requestVersion.restore(requestVersionId);

    if (request) {
      // Refresh app to reflect changes. Using timeout because we need to
      // wait for the request update to propagate.
      setTimeout(() => this._wrapper._forceRequestPaneRefresh(), 500);
    } else {
      // Couldn't restore request. That's okay
    }
  }

  _requestCreateForWorkspace() {
    this._requestCreate(this.props.activeWorkspace._id);
  }

  _startDragSidebar() {
    this.setState({ draggingSidebar: true });
  }

  _resetDragSidebar() {
    // TODO: Remove setTimeout need be not triggering drag on double click
    setTimeout(() => this._handleSetSidebarWidth(DEFAULT_SIDEBAR_WIDTH), 50);
  }

  _startDragPaneHorizontal() {
    this.setState({ draggingPaneHorizontal: true });
  }

  _startDragPaneVertical() {
    this.setState({ draggingPaneVertical: true });
  }

  _resetDragPaneHorizontal() {
    // TODO: Remove setTimeout need be not triggering drag on double click
    setTimeout(() => this._handleSetPaneWidth(DEFAULT_PANE_WIDTH), 50);
  }

  _resetDragPaneVertical() {
    // TODO: Remove setTimeout need be not triggering drag on double click
    setTimeout(() => this._handleSetPaneHeight(DEFAULT_PANE_HEIGHT), 50);
  }

  _handleMouseMove(e) {
    if (this.state.draggingPaneHorizontal) {
      // Only pop the overlay after we've moved it a bit (so we don't block doubleclick);
      const distance = this.props.paneWidth - this.state.paneWidth;
      if (!this.state.showDragOverlay && Math.abs(distance) > 0.02 /* % */) {
        this.setState({ showDragOverlay: true });
      }

      const requestPane = ReactDOM.findDOMNode(this._requestPane);
      const responsePane = ReactDOM.findDOMNode(this._responsePane);

      const requestPaneWidth = requestPane.offsetWidth;
      const responsePaneWidth = responsePane.offsetWidth;

      const pixelOffset = e.clientX - requestPane.offsetLeft;
      let paneWidth = pixelOffset / (requestPaneWidth + responsePaneWidth);
      paneWidth = Math.min(Math.max(paneWidth, MIN_PANE_WIDTH), MAX_PANE_WIDTH);

      this._handleSetPaneWidth(paneWidth);
    } else if (this.state.draggingPaneVertical) {
      // Only pop the overlay after we've moved it a bit (so we don't block doubleclick);
      const distance = this.props.paneHeight - this.state.paneHeight;
      if (!this.state.showDragOverlay && Math.abs(distance) > 0.02 /* % */) {
        this.setState({ showDragOverlay: true });
      }

      const requestPane = ReactDOM.findDOMNode(this._requestPane);
      const responsePane = ReactDOM.findDOMNode(this._responsePane);

      const requestPaneHeight = requestPane.offsetHeight;
      const responsePaneHeight = responsePane.offsetHeight;

      const pixelOffset = e.clientY - requestPane.offsetTop;
      let paneHeight = pixelOffset / (requestPaneHeight + responsePaneHeight);
      paneHeight = Math.min(Math.max(paneHeight, MIN_PANE_HEIGHT), MAX_PANE_HEIGHT);

      this._handleSetPaneHeight(paneHeight);
    } else if (this.state.draggingSidebar) {
      // Only pop the overlay after we've moved it a bit (so we don't block doubleclick);
      const distance = this.props.sidebarWidth - this.state.sidebarWidth;
      if (!this.state.showDragOverlay && Math.abs(distance) > 2 /* ems */) {
        this.setState({ showDragOverlay: true });
      }

      const sidebar = ReactDOM.findDOMNode(this._sidebar);
      const currentPixelWidth = sidebar.offsetWidth;
      const ratio = (e.clientX - sidebar.offsetLeft) / currentPixelWidth;
      const width = this.state.sidebarWidth * ratio;

      let sidebarWidth = Math.min(width, MAX_SIDEBAR_REMS);

      if (sidebarWidth < COLLAPSE_SIDEBAR_REMS) {
        sidebarWidth = MIN_SIDEBAR_REMS;
      }

      this._handleSetSidebarWidth(sidebarWidth);
    }
  }

  _handleMouseUp() {
    if (this.state.draggingSidebar) {
      this.setState({ draggingSidebar: false, showDragOverlay: false });
    }

    if (this.state.draggingPaneHorizontal) {
      this.setState({ draggingPaneHorizontal: false, showDragOverlay: false });
    }

    if (this.state.draggingPaneVertical) {
      this.setState({ draggingPaneVertical: false, showDragOverlay: false });
    }
  }

  _handleKeyDown(e) {
    for (const [definition, callback] of this._globalKeyMap) {
      executeHotKey(e, definition, callback);
    }
  }

  _handleToggleMenuBar(hide) {
    for (const win of remote.BrowserWindow.getAllWindows()) {
      if (win.isMenuBarAutoHide() !== hide) {
        win.setAutoHideMenuBar(hide);
        win.setMenuBarVisibility(!hide);
      }
    }
  }

  async _handleToggleSidebar() {
    const sidebarHidden = !this.props.sidebarHidden;
    await this._handleSetSidebarHidden(sidebarHidden);
  }

  _handleShowExportRequestsModal() {
    showModal(ExportRequestsModal);
  }

  _setWrapperRef(n) {
    this._wrapper = n;
  }

  async _handleReloadPlugins() {
    const { settings } = this.props;
    await plugins.getPlugins(true);
    templating.reload();
    themes.setTheme(settings.theme);
    console.log('[plugins] reloaded');
  }

  /**
   * Update document.title to be "Workspace (Environment) – Request"
   * @private
   */
  _updateDocumentTitle() {
    const { activeWorkspace, activeEnvironment, activeRequest } = this.props;

    let title = activeWorkspace.name;

    if (activeEnvironment) {
      title += ` (${activeEnvironment.name})`;
    }

    if (activeRequest) {
      title += ` – ${activeRequest.name}`;
    }

    document.title = title;
  }

  componentDidUpdate(prevProps) {
    this._updateDocumentTitle();

    // Force app refresh if login state changes
    if (prevProps.isLoggedIn !== this.props.isLoggedIn) {
      this.setState(state => ({
        forceRefreshCounter: state.forceRefreshCounter + 1,
      }));
    }
  }

  async _updateVCS(activeWorkspace) {
    // Get the vcs and set it to null in the state while we update it
    let vcs = this.state.vcs;
    this.setState({ vcs: null });

    if (!vcs) {
      const directory = path.join(getDataDirectory(), 'version-control');
      const driver = new FileSystemDriver({ directory });
      vcs = new VCS(driver, async conflicts => {
        return new Promise(resolve => {
          showModal(SyncMergeModal, {
            conflicts,
            handleDone: conflicts => resolve(conflicts),
          });
        });
      });
    }

    await vcs.switchProject(activeWorkspace._id);

    this.setState({ vcs });
  }

  async componentDidMount() {
    // Bind mouse and key handlers
    document.addEventListener('mouseup', this._handleMouseUp);
    document.addEventListener('mousemove', this._handleMouseMove);
    this._setGlobalKeyMap();

    // Update title
    this._updateDocumentTitle();

    // Update VCS
    await this._updateVCS(this.props.activeWorkspace);

    db.onChange(async changes => {
      let needsRefresh = false;

      for (const change of changes) {
        const [type, doc, fromSync] = change;

        const { vcs } = this.state;
        const { activeRequest } = this.props;

        // Force refresh if environment changes
        // TODO: Only do this for environments in this workspace (not easy because they're nested)
        if (doc.type === models.environment.type) {
          console.log('[App] Forcing update from environment change', change);
          needsRefresh = true;
        }

        // Force refresh if sync changes the active request
        if (fromSync && activeRequest && doc._id === activeRequest._id) {
          needsRefresh = true;
          console.log('[App] Forcing update from request change', change);
        }

        // Delete VCS project if workspace deleted
        if (vcs && doc.type === models.workspace.type && type === db.CHANGE_REMOVE) {
          await vcs.removeProjectsForRoot(doc._id);
        }
      }

      if (needsRefresh) {
        setTimeout(() => {
          this._wrapper && this._wrapper._forceRequestPaneRefresh();
        }, 300);
      }
    });

    ipcRenderer.on('toggle-preferences', () => {
      showModal(SettingsModal);
    });

    ipcRenderer.on('reload-plugins', this._handleReloadPlugins);

    ipcRenderer.on('toggle-preferences-shortcuts', () => {
      showModal(SettingsModal, TAB_INDEX_SHORTCUTS);
    });

    ipcRenderer.on('run-command', (e, commandUri) => {
      const parsed = urlParse(commandUri, true);

      const command = `${parsed.hostname}${parsed.pathname}`;
      const args = JSON.parse(JSON.stringify(parsed.query));
      args.workspaceId = args.workspaceId || this.props.activeWorkspace._id;

      this.props.handleCommand(command, args);
    });

    // NOTE: This is required for "drop" event to trigger.
    document.addEventListener(
      'dragover',
      e => {
        e.preventDefault();
      },
      false,
    );

    document.addEventListener(
      'drop',
      async e => {
        e.preventDefault();
        const { activeWorkspace, handleImportUriToWorkspace } = this.props;
        if (!activeWorkspace) {
          return;
        }

        if (e.dataTransfer.files.length === 0) {
          console.log('[drag] Ignored drop event because no files present');
          return;
        }

        const file = e.dataTransfer.files[0];
        const { path } = file;
        const uri = `file://${path}`;

        await showAlert({
          title: 'Confirm Data Import',
          message: (
            <span>
              Import <code>{path}</code>?
            </span>
          ),
          addCancel: true,
        });

        handleImportUriToWorkspace(activeWorkspace._id, uri);
      },
      false,
    );

    ipcRenderer.on('toggle-sidebar', this._handleToggleSidebar);

    // handle this
    this._handleToggleMenuBar(this.props.settings.autoHideMenuBar);

    // Give it a bit before letting the backend know it's ready
    setTimeout(() => ipcRenderer.send('window-ready'), 500);
  }

  // eslint-disable-next-line camelcase
  UNSAFE_componentWillUnmount() {
    // Remove mouse and key handlers
    document.removeEventListener('mouseup', this._handleMouseUp);
    document.removeEventListener('mousemove', this._handleMouseMove);
  }

  _ensureWorkspaceChildren(props) {
    const { activeWorkspace, activeCookieJar, environments } = props;
    const baseEnvironments = environments.filter(e => e.parentId === activeWorkspace._id);

    // Nothing to do
    if (baseEnvironments.length && activeCookieJar) {
      return;
    }

    // We already started migrating. Let it finish.
    if (this.state.isMigratingChildren) {
      return;
    }

    // Prevent rendering of everything
    this.setState({ isMigratingChildren: true }, async () => {
      const flushId = await db.bufferChanges();
      await models.environment.getOrCreateForWorkspace(activeWorkspace);
      await models.cookieJar.getOrCreateForParentId(activeWorkspace._id);
      await db.flushChanges(flushId);

      this.setState({ isMigratingChildren: false });
    });
  }

  // eslint-disable-next-line camelcase
  UNSAFE_componentWillReceiveProps(nextProps) {
    this._ensureWorkspaceChildren(nextProps);

    // Update VCS if needed
    const { activeWorkspace } = this.props;
    if (nextProps.activeWorkspace._id !== activeWorkspace._id) {
      this._updateVCS(nextProps.activeWorkspace);
    }
  }

  // eslint-disable-next-line camelcase
  UNSAFE_componentWillMount() {
    this._ensureWorkspaceChildren(this.props);
  }

  render() {
    if (this.state.isMigratingChildren) {
      console.log('[app] Waiting for migration to complete');
      return null;
    }

    const { activeWorkspace } = this.props;

    const {
      paneWidth,
      paneHeight,
      sidebarWidth,
      isVariableUncovered,
      vcs,
      forceRefreshCounter,
      forceRefreshHeaderCounter,
    } = this.state;

    const uniquenessKey = `${forceRefreshCounter}::${activeWorkspace._id}`;

    return (
      <KeydownBinder onKeydown={this._handleKeyDown}>
        <div className="app" key={uniquenessKey}>
          <ErrorBoundary showAlert>
            <Wrapper
              {...this.props}
              ref={this._setWrapperRef}
              paneWidth={paneWidth}
              paneHeight={paneHeight}
              sidebarWidth={sidebarWidth}
              handleCreateRequestForWorkspace={this._requestCreateForWorkspace}
              handleSetRequestPinned={this._handleSetRequestPinned}
              handleSetRequestGroupCollapsed={this._handleSetRequestGroupCollapsed}
              handleActivateRequest={this._handleSetActiveRequest}
              handleSetRequestPaneRef={this._setRequestPaneRef}
              handleSetResponsePaneRef={this._setResponsePaneRef}
              handleSetSidebarRef={this._setSidebarRef}
              handleStartDragSidebar={this._startDragSidebar}
              handleResetDragSidebar={this._resetDragSidebar}
              handleStartDragPaneHorizontal={this._startDragPaneHorizontal}
              handleStartDragPaneVertical={this._startDragPaneVertical}
              handleResetDragPaneHorizontal={this._resetDragPaneHorizontal}
              handleResetDragPaneVertical={this._resetDragPaneVertical}
              handleCreateRequest={this._requestCreate}
              handleRender={this._handleRenderText}
              handleGetRenderContext={this._handleGetRenderContext}
              handleDuplicateRequest={this._requestDuplicate}
              handleDuplicateRequestGroup={App._requestGroupDuplicate}
              handleMoveRequestGroup={App._requestGroupMove}
              handleDuplicateWorkspace={this._workspaceDuplicate}
              handleCreateRequestGroup={this._requestGroupCreate}
              handleGenerateCode={App._handleGenerateCode}
              handleGenerateCodeForActiveRequest={this._handleGenerateCodeForActiveRequest}
              handleCopyAsCurl={this._handleCopyAsCurl}
              handleSetResponsePreviewMode={this._handleSetResponsePreviewMode}
              handleSetResponseFilter={this._handleSetResponseFilter}
              handleSendRequestWithEnvironment={this._handleSendRequestWithEnvironment}
              handleSendAndDownloadRequestWithEnvironment={
                this._handleSendAndDownloadRequestWithEnvironment
              }
              handleSetActiveResponse={this._handleSetActiveResponse}
              handleSetActiveRequest={this._handleSetActiveRequest}
              handleSetActiveEnvironment={this._handleSetActiveEnvironment}
              handleSetSidebarFilter={this._handleSetSidebarFilter}
              handleToggleMenuBar={this._handleToggleMenuBar}
              handleUpdateRequestMimeType={this._handleUpdateRequestMimeType}
              handleShowExportRequestsModal={this._handleShowExportRequestsModal}
              handleUpdateDownloadPath={this._handleUpdateDownloadPath}
              isVariableUncovered={isVariableUncovered}
              headerEditorKey={forceRefreshHeaderCounter + ''}
              vcs={vcs}
            />
          </ErrorBoundary>

          <ErrorBoundary showAlert>
            <Toast />
          </ErrorBoundary>

          {/* Block all mouse activity by showing an overlay while dragging */}
          {this.state.showDragOverlay ? <div className="blocker-overlay" /> : null}
        </div>
      </KeydownBinder>
    );
  }
}

App.propTypes = {
  // Required
  sidebarWidth: PropTypes.number.isRequired,
  paneWidth: PropTypes.number.isRequired,
  paneHeight: PropTypes.number.isRequired,
  handleCommand: PropTypes.func.isRequired,
  settings: PropTypes.object.isRequired,
  isLoggedIn: PropTypes.bool.isRequired,
  activeWorkspace: PropTypes.shape({
    _id: PropTypes.string.isRequired,
  }).isRequired,
  handleSetActiveWorkspace: PropTypes.func.isRequired,

  // Optional
  activeRequest: PropTypes.object,
  activeEnvironment: PropTypes.shape({
    _id: PropTypes.string.isRequired,
  }),
};

function mapStateToProps(state, props) {
  const { entities, global } = state;

  const { isLoading, loadingRequestIds, isLoggedIn } = global;

  // Entities
  const entitiesLists = selectEntitiesLists(state, props);
  const {
    workspaces,
    environments,
    requests,
    requestGroups,
    requestMetas,
    requestVersions,
  } = entitiesLists;

  const settings = entitiesLists.settings[0];

  // Workspace stuff
  const workspaceMeta = selectActiveWorkspaceMeta(state, props) || {};
  const activeWorkspace = selectActiveWorkspace(state, props);
  const activeWorkspaceClientCertificates = selectActiveWorkspaceClientCertificates(state, props);
  const sidebarHidden = workspaceMeta.sidebarHidden || false;
  const sidebarFilter = workspaceMeta.sidebarFilter || '';
  const sidebarWidth = workspaceMeta.sidebarWidth || DEFAULT_SIDEBAR_WIDTH;
  const paneWidth = workspaceMeta.paneWidth || DEFAULT_PANE_WIDTH;
  const paneHeight = workspaceMeta.paneHeight || DEFAULT_PANE_HEIGHT;

  // Request stuff
  const requestMeta = selectActiveRequestMeta(state, props) || {};
  const activeRequest = selectActiveRequest(state, props);
  const responsePreviewMode = requestMeta.previewMode || PREVIEW_MODE_SOURCE;
  const responseFilter = requestMeta.responseFilter || '';
  const responseFilterHistory = requestMeta.responseFilterHistory || [];
  const responseDownloadPath = requestMeta.downloadPath || null;

  // Cookie Jar
  const activeCookieJar = selectActiveCookieJar(state, props);

  // Response stuff
  const activeRequestResponses = selectActiveRequestResponses(state, props) || [];
  const activeResponse = selectActiveResponse(state, props) || null;

  // Environment stuff
  const activeEnvironmentId = workspaceMeta.activeEnvironmentId;
  const activeEnvironment = entities.environments[activeEnvironmentId];

  // OAuth2Token stuff
  const oAuth2Token = selectActiveOAuth2Token(state, props);

  // Find other meta things
  const loadStartTime = loadingRequestIds[activeRequest ? activeRequest._id : 'n/a'] || -1;
  const sidebarChildren = selectSidebarChildren(state, props);
  const workspaceChildren = selectWorkspaceRequestsAndRequestGroups(state, props);
  const unseenWorkspaces = selectUnseenWorkspaces(state, props);

  // Sync stuff
  const syncItems = selectSyncItems(state, props);

  return Object.assign({}, state, {
    activeCookieJar,
    activeEnvironment,
    activeRequest,
    activeRequestResponses,
    activeResponse,
    activeWorkspace,
    activeWorkspaceClientCertificates,
    environments,
    isLoading,
    isLoggedIn,
    loadStartTime,
    oAuth2Token,
    paneHeight,
    paneWidth,
    requestGroups,
    requestMetas,
    requestVersions,
    requests,
    responseDownloadPath,
    responseFilter,
    responseFilterHistory,
    responsePreviewMode,
    settings,
    sidebarChildren,
    sidebarFilter,
    sidebarHidden,
    sidebarWidth,
    syncItems,
    unseenWorkspaces,
    workspaceChildren,
    workspaces,
  });
}

function mapDispatchToProps(dispatch) {
  const global = bindActionCreators(globalActions, dispatch);

  return {
    handleStartLoading: global.loadRequestStart,
    handleStopLoading: global.loadRequestStop,

    handleSetActiveWorkspace: global.setActiveWorkspace,
    handleImportFileToWorkspace: global.importFile,
    handleImportClipBoardToWorkspace: global.importClipBoard,
    handleImportUriToWorkspace: global.importUri,
    handleCommand: global.newCommand,
    handleExportFile: global.exportWorkspacesToFile,
    handleExportRequestsToFile: global.exportRequestsToFile,
    handleMoveDoc: _moveDoc,
  };
}

async function _moveDoc(docToMove, parentId, targetId, targetOffset) {
  // Nothing to do. We are in the same spot as we started
  if (docToMove._id === targetId) {
    return;
  }

  // Don't allow dragging things into itself or children. This will disconnect
  // the node from the tree and cause the item to no longer show in the UI.
  const descendents = await db.withDescendants(docToMove);
  for (const doc of descendents) {
    if (doc._id === parentId) {
      return;
    }
  }

  function __updateDoc(doc, patch) {
    models.getModel(docToMove.type).update(doc, patch);
  }

  if (targetId === null) {
    // We are moving to an empty area. No sorting required
    await __updateDoc(docToMove, { parentId });
    return;
  }

  // NOTE: using requestToTarget's parentId so we can switch parents!
  let docs = [
    ...(await models.request.findByParentId(parentId)),
    ...(await models.requestGroup.findByParentId(parentId)),
  ].sort((a, b) => (a.metaSortKey < b.metaSortKey ? -1 : 1));

  // Find the index of doc B so we can re-order and save everything
  for (let i = 0; i < docs.length; i++) {
    const doc = docs[i];

    if (doc._id === targetId) {
      let before, after;
      if (targetOffset < 0) {
        // We're moving to below
        before = docs[i];
        after = docs[i + 1];
      } else {
        // We're moving to above
        before = docs[i - 1];
        after = docs[i];
      }

      const beforeKey = before ? before.metaSortKey : docs[0].metaSortKey - 100;
      const afterKey = after ? after.metaSortKey : docs[docs.length - 1].metaSortKey + 100;

      if (Math.abs(afterKey - beforeKey) < 0.000001) {
        // If sort keys get too close together, we need to redistribute the list. This is
        // not performant at all (need to update all siblings in DB), but it is extremely rare
        // anyway
        console.log(`[app] Recreating Sort Keys ${beforeKey} ${afterKey}`);

        await db.bufferChanges(300);
        docs.map((r, i) => __updateDoc(r, { metaSortKey: i * 100, parentId }));
      } else {
        const metaSortKey = afterKey - (afterKey - beforeKey) / 2;
        __updateDoc(docToMove, { metaSortKey, parentId });
      }

      break;
    }
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps,
)(App);


export function init() {
  process.on('uncaughtException', err => {
    console.error('[catcher] Uncaught exception:', err.stack);
  });

  process.on('unhandledRejection', err => {
    console.error('[catcher] Unhandled rejection:', err.stack);
  });
}


module.exports.setDefaults = function(obj) {
  if (!obj || !obj._type) {
    return obj;
  } else if (obj._type === 'request') {
    return module.exports.setDefaultsRequest(obj);
  } else if (obj._type === 'request_group') {
    return module.exports.setDefaultsRequestGroup(obj);
  } else if (obj._type === 'environment') {
    return module.exports.setDefaultsEnvironment(obj);
  } else {
    return obj;
  }
};

module.exports.setDefaultsRequest = function(request) {
  request.method = (request.method || 'GET').toUpperCase();
  return Object.assign(
    {
      parentId: '__WORKSPACE_ID__',
      name: 'Imported',
      url: '',
      body: '',
      method: 'GET',
      parameters: [],
      headers: [],
      authentication: {},
    },
    request,
  );
};

module.exports.setDefaultsRequestGroup = function(requestGroup) {
  return Object.assign(
    {
      parentId: '__WORKSPACE_ID__',
      name: 'Imported',
      environment: {},
    },
    requestGroup,
  );
};

module.exports.setDefaultsEnvironment = function(environment) {
  return Object.assign(
    {
      parentId: '__BASE_ENVIRONMENT_ID__',
      name: 'Imported Environment',
      data: {},
    },
    environment,
  );
};

module.exports.getDateString = function() {
  return new Date().toISOString();
};

module.exports.unthrowableParseJson = function(rawData) {
  try {
    return JSON.parse(rawData);
  } catch (err) {
    return null;
  }
};


const fs = require('fs')
const path = require('path')

const findExisting = (context, files) => {
  for (const file of files) {
    if (fs.existsSync(path.join(context, file))) {
      return file
    }
  }
}

module.exports = (api, options) => {
  api.chainWebpack(webpackConfig => {
    const getAssetPath = require('../util/getAssetPath')
    const shadowMode = !!process.env.VUE_CLI_CSS_SHADOW_MODE
    const isProd = process.env.NODE_ENV === 'production'

    const {
      modules = false,
      extract = isProd,
      sourceMap = false,
      loaderOptions = {}
    } = options.css || {}

    const shouldExtract = extract !== false && !shadowMode
    const filename = getAssetPath(
      options,
      `css/[name]${options.filenameHashing ? '.[contenthash:8]' : ''}.css`,
      true /* placeAtRootIfRelative */
    )
    const extractOptions = Object.assign({
      filename,
      chunkFilename: filename
    }, extract && typeof extract === 'object' ? extract : {})

    // check if the project has a valid postcss config
    // if it doesn't, don't use postcss-loader for direct style imports
    // because otherwise it would throw error when attempting to load postcss config
    const hasPostCSSConfig = !!(api.service.pkg.postcss || findExisting(api.resolve('.'), [
      '.postcssrc',
      '.postcssrc.js',
      'postcss.config.js',
      '.postcssrc.yaml',
      '.postcssrc.json'
    ]))

    function createCSSRule (lang, test, loader, options) {
      const baseRule = webpackConfig.module.rule(lang).test(test)

      // rules for <style lang="module">
      const vueModulesRule = baseRule.oneOf('vue-modules').resourceQuery(/module/)
      applyLoaders(vueModulesRule, true)

      // rules for <style>
      const vueNormalRule = baseRule.oneOf('vue').resourceQuery(/\?vue/)
      applyLoaders(vueNormalRule, false)

      // rules for *.module.* files
      const extModulesRule = baseRule.oneOf('normal-modules').test(/\.module\.\w+$/)
      applyLoaders(extModulesRule, true)

      // rules for normal CSS imports
      const normalRule = baseRule.oneOf('normal')
      applyLoaders(normalRule, modules)

      function applyLoaders (rule, modules) {
        if (shouldExtract) {
          rule
            .use('extract-css-loader')
            .loader(require('mini-css-extract-plugin').loader)
        } else {
          rule
            .use('vue-style-loader')
            .loader('vue-style-loader')
            .options({
              sourceMap,
              shadowMode
            })
        }

        const cssLoaderOptions = Object.assign({
          sourceMap,
          importLoaders: (
            1 + // stylePostLoader injected by vue-loader
            hasPostCSSConfig +
            !!loader
          )
        }, loaderOptions.css)

        if (modules) {
          const {
            localIdentName = '[name]_[local]_[hash:base64:5]'
          } = loaderOptions.css || {}
          Object.assign(cssLoaderOptions, {
            modules,
            localIdentName
          })
        }

        rule
          .use('css-loader')
          .loader('css-loader')
          .options(cssLoaderOptions)

        if (hasPostCSSConfig) {
          rule
            .use('postcss-loader')
            .loader('postcss-loader')
            .options(Object.assign({ sourceMap }, loaderOptions.postcss))
        }

        if (loader) {
          rule
            .use(loader)
            .loader(loader)
            .options(Object.assign({ sourceMap }, options))
        }
      }
    }

    createCSSRule('css', /\.css$/)
    createCSSRule('postcss', /\.p(ost)?css$/)
    createCSSRule('scss', /\.scss$/, 'sass-loader', loaderOptions.sass)
    createCSSRule('sass', /\.sass$/, 'sass-loader', Object.assign({
      indentedSyntax: true
    }, loaderOptions.sass))
    createCSSRule('less', /\.less$/, 'less-loader', loaderOptions.less)
    createCSSRule('stylus', /\.styl(us)?$/, 'stylus-loader', Object.assign({
      preferPathResolver: 'webpack'
    }, loaderOptions.stylus))

    // inject CSS extraction plugin
    if (shouldExtract) {
      webpackConfig
        .plugin('extract-css')
          .use(require('mini-css-extract-plugin'), [extractOptions])
    }

    if (isProd) {
      // optimize CSS (dedupe)
      const cssProcessorOptions = {
        safe: true,
        autoprefixer: { disable: true },
        mergeLonghand: false
      }
      if (options.productionSourceMap && sourceMap) {
        cssProcessorOptions.map = { inline: false }
      }
      webpackConfig
        .plugin('optimize-css')
          .use(require('@intervolga/optimize-cssnano-plugin'), [{
            sourceMap: options.productionSourceMap && sourceMap,
            cssnanoOptions: cssProcessorOptions
          }])
    }
  })
}


const gql = require('graphql-tag')
// Subs
const { withFilter } = require('graphql-subscriptions')
const channels = require('../channels')
// Connectors
const tasks = require('../connectors/tasks')
const plugins = require('../connectors/plugins')
const projects = require('../connectors/projects')

exports.types = gql`
extend type Query {
  tasks: [Task]
  task (id: ID!): Task
}

extend type Mutation {
  taskRun (id: ID!): Task
  taskStop (id: ID!): Task
  taskLogsClear (id: ID!): Task
  taskOpen (id: ID!): Boolean
  taskSaveParameters (id: ID!): [Prompt]
  taskRestoreParameters (id: ID!): [Prompt]
}

extend type Subscription {
  taskChanged: Task
  taskLogAdded (id: ID!): TaskLog
}

type Task implements DescribedEntity {
  id: ID!
  status: TaskStatus!
  command: String!
  name: String
  description: String
  link: String
  icon: String
  logs: [TaskLog]
  prompts: [Prompt]
  views: [TaskView]
  defaultView: String
  plugin: Plugin
  project: Project
}

enum TaskStatus {
  idle
  running
  done
  error
  terminated
}

type TaskLog {
  taskId: ID!
  type: TaskLogType!
  text: String
}

enum TaskLogType {
  stdout
  stderr
}

type TaskView {
  id: ID!
  label: String!
  component: String!
  icon: String
}
`

exports.resolvers = {
  Task: {
    prompts: (task, args, context) => tasks.getPrompts(task.id, context),
    plugin: (task, args, context) => plugins.findOne({ id: task.pluginId, file: task.path }, context),
    project: (task, args, context) => projects.findByPath(task.path, context)
  },

  Query: {
    tasks: (root, args, context) => tasks.list(undefined, context),
    task: (root, { id }, context) => tasks.findOne(id, context)
  },

  Mutation: {
    taskRun: (root, { id }, context) => tasks.run(id, context),
    taskStop: (root, { id }, context) => tasks.stop(id, context),
    taskLogsClear: (root, { id }, context) => tasks.clearLogs(id, context),
    taskOpen: (root, { id }, context) => tasks.open(id, context),
    taskSaveParameters: (root, { id }, context) => tasks.saveParameters({ id }, context),
    taskRestoreParameters: (root, { id }, context) => tasks.restoreParameters({ id }, context)
  },

  Subscription: {
    taskChanged: {
      subscribe: (parent, args, { pubsub }) => pubsub.asyncIterator(channels.TASK_CHANGED)
    },
    taskLogAdded: {
      subscribe: withFilter(
        (parent, args, { pubsub }) => pubsub.asyncIterator(channels.TASK_LOG_ADDED),
        (payload, vars) => payload.taskLogAdded.taskId === vars.id
      )
    }
  }
}


// dev only

const path = require('path')
const { linkBin } = require('./linkBin')

module.exports = function setupDevProject (targetDir) {
  return linkBin(
    require.resolve('@vue/cli-service/bin/vue-cli-service'),
    path.join(targetDir, 'node_modules', '.bin', 'vue-cli-service')
  )
}


jest.mock('fs')
jest.mock('inquirer')

const assertPromptModule = require('@vue/cli-test-utils/assertPromptModule')

const moduleToTest = require('../pwa')

test('pwa', async () => {
  const expectedPrompts = [
    {
      message: 'features',
      choices: ['PWA'],
      check: [0]
    }
  ]

  const expectedOptions = {
    plugins: {
      '@vue/cli-plugin-pwa': {}
    }
  }

  await assertPromptModule(
    moduleToTest,
    expectedPrompts,
    expectedOptions,
    { pluginsOnly: true }
  )
})