From 0cb726659981dddfa96531fd1340994a19ca2bd5 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 18 Nov 2016 23:11:10 -0800 Subject: [PATCH] Updated import screen, better curl paste, and better model defaults --- app/{export => common}/__tests__/har.test.js | 4 +- app/common/__tests__/misc.test.js | 13 ++ app/common/database.js | 13 +- app/{export => common}/har.js | 6 +- app/{export/insomnia.js => common/import.js} | 10 +- app/common/misc.js | 12 ++ app/export/curl.js | 195 ------------------ app/export/legacy.js | 66 ------ app/models/index.js | 2 + app/package.json | 2 +- app/ui/components/modals/GenerateCodeModal.js | 2 +- .../settings/SettingsImportExport.js | 9 +- app/ui/containers/App.js | 39 +++- app/ui/css/layout/base.less | 1 + app/ui/redux/modules/global.js | 8 +- package.json | 2 +- 16 files changed, 87 insertions(+), 297 deletions(-) rename app/{export => common}/__tests__/har.test.js (95%) rename app/{export => common}/har.js (90%) rename app/{export/insomnia.js => common/import.js} (92%) delete mode 100644 app/export/curl.js delete mode 100644 app/export/legacy.js diff --git a/app/export/__tests__/har.test.js b/app/common/__tests__/har.test.js similarity index 95% rename from app/export/__tests__/har.test.js rename to app/common/__tests__/har.test.js index c7e3e7e01..684789102 100644 --- a/app/export/__tests__/har.test.js +++ b/app/common/__tests__/har.test.js @@ -1,6 +1,6 @@ import * as harUtils from '../har'; -import * as db from '../../common/database'; -import * as render from '../../common/render'; +import * as db from '../database'; +import * as render from '../render'; import * as models from '../../models'; describe('exportHarWithRequest()', () => { diff --git a/app/common/__tests__/misc.test.js b/app/common/__tests__/misc.test.js index a6fd2fd1b..30a7fbdc1 100644 --- a/app/common/__tests__/misc.test.js +++ b/app/common/__tests__/misc.test.js @@ -208,3 +208,16 @@ describe('debounce()', () => { expect(resultList).toEqual([['foo', 'bar3']]); }) }); + +describe('strictObjectAssign()', () => { + it('handles assignment', () => { + const actual = misc.strictObjectAssign( + {foo: 'hi', bar: {baz: 'qux'}}, + {foo: 'hi again', a: 'b'}, + {a: 'c', foo: 'final foo'}, + ); + expect(actual).toEqual({ + foo: 'final foo', bar: {baz: 'qux'} + }); + }) +}); diff --git a/app/common/database.js b/app/common/database.js index 9186cbaf9..1f6dd007b 100644 --- a/app/common/database.js +++ b/app/common/database.js @@ -4,6 +4,7 @@ import fsPath from 'path'; import {DB_PERSIST_INTERVAL} from './constants'; import {generateId} from './misc'; import {getModel, initModel} from '../models'; +import * as misc from './misc'; export const CHANGE_INSERT = 'insert'; export const CHANGE_UPDATE = 'update'; @@ -128,7 +129,7 @@ export function find (type, query = {}) { const modelDefaults = initModel(type); const docs = rawDocs.map(rawDoc => { - return Object.assign({}, modelDefaults, rawDoc); + return Object.assign(modelDefaults, rawDoc); }); resolve(docs); @@ -153,7 +154,7 @@ export function getWhere (type, query) { } const modelDefaults = initModel(type); - resolve(Object.assign({}, modelDefaults, rawDocs[0])); + resolve(Object.assign(modelDefaults, rawDocs[0])); }) }) } @@ -245,11 +246,11 @@ export function removeBulkSilently (type, query) { // ~~~~~~~~~~~~~~~~~~~ // export function docUpdate (originalDoc, patch = {}) { - const doc = Object.assign( + const doc = misc.strictObjectAssign( initModel(originalDoc.type), originalDoc, patch, - {modified: Date.now()} + {modified: Date.now()}, ); return update(doc); @@ -262,9 +263,9 @@ export function docCreate (type, patch = {}) { throw new Error(`No ID prefix for ${type}`) } - const doc = Object.assign( - {_id: generateId(idPrefix)}, + const doc = misc.strictObjectAssign( initModel(type), + {_id: generateId(idPrefix)}, patch, // Fields that the user can't touch diff --git a/app/export/har.js b/app/common/har.js similarity index 90% rename from app/export/har.js rename to app/common/har.js index e4772225f..d5160fced 100644 --- a/app/export/har.js +++ b/app/common/har.js @@ -1,7 +1,7 @@ import * as models from '../models'; -import {getRenderedRequest} from '../common/render'; -import {jarFromCookies} from '../common/cookies'; -import * as util from '../common/misc'; +import {getRenderedRequest} from './render'; +import {jarFromCookies} from './cookies'; +import * as util from './misc'; export function exportHarWithRequest (renderedRequest, addContentLength = false) { if (addContentLength) { diff --git a/app/export/insomnia.js b/app/common/import.js similarity index 92% rename from app/export/insomnia.js rename to app/common/import.js index 519b4404d..de9c37207 100644 --- a/app/export/insomnia.js +++ b/app/common/import.js @@ -1,8 +1,8 @@ import * as importers from 'insomnia-importers'; -import * as db from '../common/database'; +import * as db from './database'; import * as models from '../models'; -import {getAppVersion} from '../common/constants'; -import * as misc from '../common/misc'; +import {getAppVersion} from './constants'; +import * as misc from './misc'; const EXPORT_TYPE_REQUEST = 'request'; const EXPORT_TYPE_REQUEST_GROUP = 'request_group'; @@ -21,10 +21,10 @@ const MODELS = { [EXPORT_TYPE_ENVIRONMENT]: models.environment, }; -export async function importJSON (workspace, json, generateNewIds = false) { +export async function importRaw (workspace, rawContent, generateNewIds = false) { let data; try { - data = importers.import(json); + data = importers.import(rawContent); } catch (e) { console.error('Failed to import data', e); return; diff --git a/app/common/misc.js b/app/common/misc.js index ac0a315e5..edfb9df3e 100644 --- a/app/common/misc.js +++ b/app/common/misc.js @@ -149,3 +149,15 @@ export function debounce (callback, millis = DEBOUNCE_MILLIS) { callback.apply(null, results['__key__']) }, millis).bind(null, '__key__'); } + +/** Same as Object.assign but only add fields that exist on target */ +export function strictObjectAssign (target, ...sources) { + const patch = Object.assign(...sources); + for (const key of Object.keys(target)) { + if (patch.hasOwnProperty(key)) { + target[key] = patch[key]; + } + } + + return target; +} diff --git a/app/export/curl.js b/app/export/curl.js deleted file mode 100644 index c2b316d01..000000000 --- a/app/export/curl.js +++ /dev/null @@ -1,195 +0,0 @@ -const FLAGS = [ - 'cacert', 'capath', 'E', 'cert', 'cert-type', 'ciphers', 'K', 'config', - 'connect-timeout', 'C', 'continue-at', 'b', - 'cookie', // TODO: Handle this - 'c', 'cookie-jar', 'crlfile', 'd', 'data', 'data-ascii', 'data-binary', - 'data-urlencode', 'delegation', 'D', 'dump-header', 'egd-file', - 'engine', 'F', 'form', 'form-string', 'ftp-account', 'ftp-method', - 'ftp-port', 'H', 'header', 'hostpubmd5', 'interface', 'keepalive-time', - 'key', 'key-type', 'krb', 'lib-curl', 'limit-rate', 'local-port', - 'mail-from', 'mail-rcpt', 'mail-auth', 'max-filesize', 'max-redirs', - 'max-time', 'netrtc-file', 'output', 'pass', 'proto', 'proto-redir', - 'proxy', 'proxy-user', 'proxy1.0', 'pubkey', 'Q', 'quote', - 'random-file', 'range', 'X', 'request', 'resolve', 'retry', - 'retry-delay', 'retry-max-time', 'socks4', 'socks4a', 'socks5', - 'socks5-hostname', 'socks5-gssapi-service', 'Y', 'speed-limit', - 'y', 'speed-time', 'stderr', 'tftp-blksize', 'z', 'time-cond', 'trace', - 'trace-ascii', 'T', 'upload-file', - 'url', - 'u', 'user', 'tlsuser', 'tlspassword', 'tlsauthtype', - 'A', 'user-agent', // TODO: Handle this - 'w', 'write-out' -]; - -const FLAG_REGEXES = [ - /\s--([\w\-]{2,})\s+"((?:[^"\\]|\\.)*)"/, // --my-flag "hello" - /\s--([\w\-]{2,})\s+'((?:[^'\\]|\\.)*)'/, // --my-flag 'hello' - /\s--([\w\-]{2,})\s+([^\s]+)/, // --my-flag hello - /\s-([\w])\s*'((?:[^'\\]|\\.)*)'/, // -X 'hello' - /\s-([\w])\s*"((?:[^"\\]|\\.)*)"/, // -X "hello" - /\s-([\w])\s*([^\s]+)/, // -X hello - /\s--([\w\-]+)/ // --switch (cleanup at the end) -]; - -function getFlags (cmd) { - const flags = {}; - const switches = {}; - - for (var i = 0; i < FLAG_REGEXES.length; i++) { - var matches = []; - var match; - var key; - var val; - var matchedFlag; - - // Stop at 1000 to prevent infinite loops - // TODO: Make this recursive - for (var j = 0; (matches = FLAG_REGEXES[i].exec(cmd)); j++) { - if (j > 1000) { - console.error('INFINITE LOOP'); - break; - } - - match = matches[0]; - key = matches[1]; - val = matches[2]; - matchedFlag = !!val; - - if (matchedFlag) { - if (isFlag(key)) { - // Matched a flag - flags[key] = flags[key] || []; - flags[key].push(val); - cmd = cmd.replace(match, ''); - } else { - // Matched a flag that was actually a switch - cmd = cmd.replace('--' + key, ''); - } - } else { - // Matched a switch directly without a value - switches[key] = true; - cmd = cmd.replace(match, ''); - } - } - } - - return {cmd, flags, switches}; -} - -function isFlag (key) { - for (var i = 0; i < FLAGS.length; i++) { - if (key === FLAGS[i]) { - return true; - } - } - return false; -} - -function splitHeaders (flags) { - var headers = (flags['H'] || []).concat(flags['header'] || []); - var parsed = []; - - for (var i = 0; i < headers.length; i++) { - var header = headers[i].split(':'); - var name = header[0].trim(); - var value = header[1].trim(); - - parsed.push({name: name, value: value}); - } - - return parsed; -} - -function getContentType (headers) { - for (var i = 0; i < headers.length; i++) { - var header = headers[i]; - if (header.name.toLowerCase() === 'content-type') { - return header.value; - } - } - return null; -} - -function getHttpMethod (flags, hasBody) { - var method = (flags['X'] || flags['request'] || [])[0]; - - // If there is no method specified, but there is a body, default - // to POST, else default to GET - if (!method) { - method = hasBody ? 'POST' : 'GET'; - } - - return method; -} - -function getBasicAuth (flags) { - var authString = flags.u || flags.user; - var auth = { - username: '', - password: '' - }; - - if (authString) { - var authSplit = authString[0].split(':'); - auth.username = (authSplit[0] || '').trim(); - auth.password = (authSplit[1] || '').trim(); - } - - return auth; -} - - -export function importCurl (blob) { - if (!blob || blob.toLowerCase().indexOf('curl ') !== 0) { - return false; - } - - // Rip out the flags - let {cmd, flags, switches} = getFlags(blob); - - // Final values - const headers = splitHeaders(flags); - - let body = ( - flags.d || - flags.data || - flags['data-binary'] || - flags['data-ascii'] || - [] - )[0] || ''; - - const contentType = getContentType(headers) || null; - - if (contentType && contentType.toLowerCase() === 'application/json') { - try { - body = JSON.stringify(JSON.parse(body), null, '\t'); - } catch (e) { - } - } - - const httpMethod = getHttpMethod(flags, !!body); - const authentication = getBasicAuth(flags); - - // Clean up the remaining URL - cmd = (cmd + ' ') - .replace(/\\ /g, ' ').replace(/ \\/g, ' ') // slashes - .replace(/\s/g, ' ') // whitespaces - .replace(/ "/g, ' ').replace(/" /g, ' ') // double-quotes - .replace(/ '/g, ' ').replace(/' /g, ' '); // single-quotes - - let url; - if (flags.url && flags.url.length) { - url = flags.url[0]; - } else { - url = /curl\s+['"]?((?!('|")).*)['"]?/.exec(cmd)[1].trim(); - } - - return { - url: url, - body: body, - headers: headers, - contentType: contentType, - method: httpMethod, - authentication: authentication - }; -} diff --git a/app/export/legacy.js b/app/export/legacy.js deleted file mode 100644 index 96132d94e..000000000 --- a/app/export/legacy.js +++ /dev/null @@ -1,66 +0,0 @@ -import * as models from '../models'; -import {getContentTypeFromHeaders} from '../common/constants'; - -const FORMAT_MAP = { - json: 'application/json', - xml: 'application/xml', - form: 'application/x-www-form-urlencoded', - text: 'text/plain' -}; - -export async function importRequestGroupLegacy (importedRequestGroup, parentId, index = 1) { - const requestGroup = await models.requestGroup.create({ - parentId, - name: importedRequestGroup.name, - environment: (importedRequestGroup.environments || {}).base || {}, - metaCollapsed: true, - metaSortKey: index * 1000 - }); - - // Sometimes (maybe all the time, I can't remember) requests will be nested - if (importedRequestGroup.hasOwnProperty('requests')) { - // Let's process them oldest to newest - importedRequestGroup.requests.map( - (r, i) => importRequestLegacy(r, requestGroup._id, index * 1000 + i) - ); - } -} - -export function importRequestLegacy (importedRequest, parentId, index = 1) { - let auth = {}; - if (importedRequest.authentication.username) { - auth = { - username: importedRequest.authentication.username, - password: importedRequest.authentication.password - } - } - - // Add the content type header - const headers = importedRequest.headers || []; - const contentType = getContentTypeFromHeaders(headers); - if (!contentType) { - const derivedContentType = FORMAT_MAP[importedRequest.__insomnia.format]; - - if (derivedContentType) { - headers.push({ - name: 'Content-Type', - value: FORMAT_MAP[importedRequest.__insomnia.format] - }); - } - } - - models.request.create({ - parentId, - _id: importedRequest._id, - name: importedRequest.name, - url: importedRequest.url, - method: importedRequest.method, - body: importedRequest.body, - headers: headers, - parameters: importedRequest.params || [], - metaSortKey: index * 1000, - contentType: FORMAT_MAP[importedRequest.__insomnia.format] || 'text/plain', - authentication: auth - }); -} - diff --git a/app/models/index.js b/app/models/index.js index 182cda665..4a384373b 100644 --- a/app/models/index.js +++ b/app/models/index.js @@ -42,6 +42,8 @@ export function getModel (type) { export function initModel (type) { const baseDefaults = { + _id: null, + type, modified: Date.now(), created: Date.now(), parentId: null diff --git a/app/package.json b/app/package.json index e1680e27f..a4b1bbb83 100644 --- a/app/package.json +++ b/app/package.json @@ -16,7 +16,7 @@ "electron-squirrel-startup": "^1.0.0", "hkdf": "0.0.2", "httpsnippet": "git@github.com:getinsomnia/httpsnippet.git#a3a2c0a0167fa844bf92df52a1442fa1d68a9053", - "insomnia-importers": "^0.1.3", + "insomnia-importers": "^0.2.0", "json-lint": "^0.1.0", "jsonpath-plus": "^0.15.0", "mime-types": "^2.1.12", diff --git a/app/ui/components/modals/GenerateCodeModal.js b/app/ui/components/modals/GenerateCodeModal.js index 3db002c9c..63028880f 100644 --- a/app/ui/components/modals/GenerateCodeModal.js +++ b/app/ui/components/modals/GenerateCodeModal.js @@ -8,7 +8,7 @@ import Modal from '../base/Modal'; import ModalBody from '../base/ModalBody'; import ModalHeader from '../base/ModalHeader'; import ModalFooter from '../base/ModalFooter'; -import {exportHar} from '../../../export/har'; +import {exportHar} from '../../../common/har'; const DEFAULT_TARGET = availableTargets().find(t => t.key === 'shell'); const DEFAULT_CLIENT = DEFAULT_TARGET.clients.find(t => t.key === 'curl'); diff --git a/app/ui/components/settings/SettingsImportExport.js b/app/ui/components/settings/SettingsImportExport.js index 297b375ad..edd0bbd33 100644 --- a/app/ui/components/settings/SettingsImportExport.js +++ b/app/ui/components/settings/SettingsImportExport.js @@ -9,8 +9,7 @@ const SettingsImportExport = ({

Data Import and Export

- Be aware that you may be exporting private data. - Also, any imported data may overwrite existing data. + Import format will be automatically detected (Insomnia, Postman, HAR, cURL)

@@ -30,9 +29,9 @@ const SettingsImportExport = ({ - {/*

*/} - {/** Tip: You can also paste Curl commands into the URL bar*/} - {/*

*/} +

+ * Tip: You can also paste Curl commands into the URL bar +

); diff --git a/app/ui/containers/App.js b/app/ui/containers/App.js index 21e93303d..7125de9eb 100644 --- a/app/ui/containers/App.js +++ b/app/ui/containers/App.js @@ -1,6 +1,7 @@ import React, {Component, PropTypes} from 'react'; import {ipcRenderer} from 'electron'; import ReactDOM from 'react-dom'; +import * as importers from 'insomnia-importers'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import HTML5Backend from 'react-dnd-html5-backend'; @@ -22,7 +23,7 @@ import * as requestMetaActions from '../redux/modules/requestMeta'; import * as requestGroupMetaActions from '../redux/modules/requestGroupMeta'; import * as db from '../../common/database'; import * as models from '../../models'; -import {importCurl} from '../../export/curl'; +import {importRaw} from '../../common/import'; import {trackEvent, trackLegacyEvent} from '../../analytics'; import {PREVIEW_MODE_SOURCE} from '../../common/constants'; @@ -130,14 +131,36 @@ class App extends Component { } async _handleUrlChanged (request, url) { - // TODO: Should this be moved elsewhere? - const requestPatch = importCurl(url); - if (requestPatch) { - await models.request.update(request, requestPatch); - this._forceHardRefresh(); - } else { - models.request.update(request, {url}); + // Allow user to paste any import file into the url. If it results in + // only one item, it will overwrite the current request. + + try { + const {resources} = importers.import(url); + const r = resources[0]; + if (r && r._type === 'request') { + const cookieHeaders = r.cookies.map(({name, value}) => ( + {name: 'cookie', value: `${name}=${value}`} + )); + + // Only pull fields that we want to update + await models.request.update(request, { + url: r.url, + method: r.method, + headers: [...r.headers, ...cookieHeaders], + body: r.body, + authentication: r.authentication, + parameters: r.parameters, + }); + + this._forceHardRefresh(); + + return; + } + } catch (e) { + // Import failed, that's alright } + + models.request.update(request, {url}); } _startDragSidebar () { diff --git a/app/ui/css/layout/base.less b/app/ui/css/layout/base.less index 424b911cf..6e1b615ec 100644 --- a/app/ui/css/layout/base.less +++ b/app/ui/css/layout/base.less @@ -102,6 +102,7 @@ code, pre, .monospace { } .notice { + text-align: center; color: @font-light-bg !important; padding: @padding-sm; border-radius: @radius-md; diff --git a/app/ui/redux/modules/global.js b/app/ui/redux/modules/global.js index d884ba2f8..edf6d1879 100644 --- a/app/ui/redux/modules/global.js +++ b/app/ui/redux/modules/global.js @@ -2,10 +2,10 @@ import electron from 'electron'; import {combineReducers} from 'redux'; import fs from 'fs'; -import {importJSON, exportJSON} from '../../../export/insomnia'; +import {importRaw, exportJSON} from '../../../common/import'; import {trackEvent} from '../../../analytics'; import AlertModal from '../../components/modals/AlertModal'; -import {showModal} from '../../components/modals/index'; +import {showModal} from '../../components/modals'; import PaymentNotificationModal from '../../components/modals/PaymentNotificationModal'; import LoginModal from '../../components/modals/LoginModal'; import * as models from '../../../models'; @@ -135,7 +135,7 @@ export function importFile (workspaceId) { properties: ['openFile'], filters: [{ // Allow empty extension and JSON - name: 'Insomnia Import', extensions: ['', 'json'] + name: 'Insomnia Import', extensions: ['', 'sh', 'txt', 'json'] }] }; @@ -158,7 +158,7 @@ export function importFile (workspaceId) { return; } - importJSON(workspace, data); + importRaw(workspace, data); trackEvent('Import', 'Success'); }); }) diff --git a/package.json b/package.json index 09cd1cc3f..fd260377c 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "electron-squirrel-startup": "^1.0.0", "hkdf": "0.0.2", "httpsnippet": "git@github.com:getinsomnia/httpsnippet.git#a3a2c0a0167fa844bf92df52a1442fa1d68a9053", - "insomnia-importers": "^0.1.3", + "insomnia-importers": "^0.2.0", "json-lint": "^0.1.0", "jsonpath-plus": "^0.15.0", "mime-types": "^2.1.12",