diff --git a/app/__tests__/package.test.js b/app/__tests__/package.test.js index 5457bb260..69025fd9e 100644 --- a/app/__tests__/package.test.js +++ b/app/__tests__/package.test.js @@ -11,8 +11,4 @@ describe('package.json', () => { expect(`${name}::${actual}`).toBe(`${name}::${expected}`); } }); - - it('pdfjs should not be in deps', () => { - expect(appPackage.dependencies['simple-react-pdf']).toBeUndefined(); - }); }); diff --git a/app/analytics/index.js b/app/analytics/index.js index 21e7affab..3068cc03b 100644 --- a/app/analytics/index.js +++ b/app/analytics/index.js @@ -1,7 +1,7 @@ import * as google from './google'; import * as models from '../models'; import {ipcRenderer} from 'electron'; -import {getAppVersion, getAppPlatform} from '../common/constants'; +import {getAppVersion, getAppPlatform, isDevelopment} from '../common/constants'; let initialized = false; export async function init (accountId) { @@ -24,7 +24,7 @@ export async function init (accountId) { trackEvent(...args); }); - if (window) { + if (window && !isDevelopment()) { window.addEventListener('error', e => { trackEvent('Error', 'Uncaught Error'); console.error('Uncaught Error', e); diff --git a/app/network/__tests__/multipart.test.js b/app/network/__tests__/multipart.test.js index 3d0e36149..d9388d1639 100644 --- a/app/network/__tests__/multipart.test.js +++ b/app/network/__tests__/multipart.test.js @@ -13,15 +13,15 @@ describe('buildMultipart()', () => { expect(boundary).toBe('------------------------X-INSOMNIA-BOUNDARY'); expect(body.toString()).toBe([ - `${boundary}`, + `--${boundary}`, 'Content-Disposition: form-data; name="foo"', '', 'bar', - `${boundary}`, + `--${boundary}`, 'Content-Disposition: form-data; name="multi-line"', '', 'Hello\nWorld!', - `${boundary}--`, + `--${boundary}--`, '' ].join('\r\n')); }); @@ -36,20 +36,20 @@ describe('buildMultipart()', () => { expect(boundary).toBe('------------------------X-INSOMNIA-BOUNDARY'); expect(body.toString()).toBe([ - `${boundary}`, + `--${boundary}`, 'Content-Disposition: form-data; name="foo"', '', 'bar', - `${boundary}`, + `--${boundary}`, 'Content-Disposition: form-data; name="file"; filename="testfile.txt"', 'Content-Type: text/plain', '', 'Hello World!\n\nHow are you?', - `${boundary}`, + `--${boundary}`, 'Content-Disposition: form-data; name="baz"', '', 'qux', - `${boundary}--`, + `--${boundary}--`, '' ].join('\r\n')); }); @@ -64,15 +64,15 @@ describe('buildMultipart()', () => { expect(boundary).toBe('------------------------X-INSOMNIA-BOUNDARY'); expect(body.toString()).toBe([ - `${boundary}`, + `--${boundary}`, 'Content-Disposition: form-data; name=""', '', 'bar', - `${boundary}`, + `--${boundary}`, 'Content-Disposition: form-data; name="foo"', '', '', - `${boundary}--`, + `--${boundary}--`, '' ].join('\r\n')); }); diff --git a/app/network/__tests__/network.test.js b/app/network/__tests__/network.test.js index 496a45176..86748f6c7 100644 --- a/app/network/__tests__/network.test.js +++ b/app/network/__tests__/network.test.js @@ -1,8 +1,9 @@ import * as networkUtils from '../network'; +import fs from 'fs'; import {join as pathJoin, resolve as pathResolve} from 'path'; import {getRenderedRequest} from '../../common/render'; import * as models from '../../models'; -import {AUTH_AWS_IAM, AUTH_BASIC, AUTH_NETRC, CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_URLENCODED, getAppVersion} from '../../common/constants'; +import {AUTH_AWS_IAM, AUTH_BASIC, AUTH_NETRC, CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, getAppVersion} from '../../common/constants'; import {filterHeaders} from '../../common/misc'; import {globalBeforeEach} from '../../__jest__/before-each'; @@ -281,74 +282,74 @@ describe('actuallySend()', () => { }); }); - // it('sends multipart form data', async () => { - // const workspace = await models.workspace.create(); - // const settings = await models.settings.create(); - // await models.cookieJar.create({parentId: workspace._id}); - // const fileName = pathResolve(pathJoin(__dirname, './testfile.txt')); - // - // const request = Object.assign(models.request.init(), { - // _id: 'req_123', - // parentId: workspace._id, - // headers: [{name: 'Content-Type', value: 'multipart/form-data'}], - // url: 'http://localhost', - // method: 'POST', - // body: { - // mimeType: CONTENT_TYPE_FORM_DATA, - // params: [ - // // Should ignore value and send the file since type is set to file - // {name: 'foo', fileName: fileName, value: 'bar', type: 'file'}, - // - // // Some extra params - // {name: 'a', value: 'AA'}, - // {name: 'baz', value: 'qux', disabled: true} - // ] - // } - // }); - // - // const renderedRequest = await getRenderedRequest(request); - // const {bodyBuffer} = await networkUtils._actuallySend( - // renderedRequest, - // workspace, - // settings - // ); - // const body = JSON.parse(bodyBuffer); - // expect(body.meta.READFUNCTION_VALUE).toBe([ - // '------------------------X-INSOMNIA-BOUNDARY', - // 'Content-Disposition: form-data; name="foo"; filename="testfile.txt"', - // 'Content-Type: text/plain', - // '', - // fs.readFileSync(fileName), - // '------------------------X-INSOMNIA-BOUNDARY', - // 'Content-Disposition: form-data; name="a"', - // '', - // 'AA', - // '------------------------X-INSOMNIA-BOUNDARY--', - // '' - // ].join('\r\n')); - // - // expect(body.options).toEqual({ - // POST: 1, - // ACCEPT_ENCODING: '', - // COOKIEFILE: '', - // FOLLOWLOCATION: true, - // MAXREDIRS: -1, - // CUSTOMREQUEST: 'POST', - // HTTPHEADER: [ - // 'Content-Type: multipart/form-data; boundary=------------------------X-INSOMNIA-BOUNDARY', - // 'Expect: ', - // 'Transfer-Encoding: ' - // ], - // INFILESIZE_LARGE: 310, - // NOPROGRESS: false, - // PROXY: '', - // TIMEOUT_MS: 0, - // URL: 'http://localhost/', - // UPLOAD: 1, - // USERAGENT: `insomnia/${getAppVersion()}`, - // VERBOSE: true - // }); - // }); + it('sends multipart form data', async () => { + const workspace = await models.workspace.create(); + const settings = await models.settings.create(); + await models.cookieJar.create({parentId: workspace._id}); + const fileName = pathResolve(pathJoin(__dirname, './testfile.txt')); + + const request = Object.assign(models.request.init(), { + _id: 'req_123', + parentId: workspace._id, + headers: [{name: 'Content-Type', value: 'multipart/form-data'}], + url: 'http://localhost', + method: 'POST', + body: { + mimeType: CONTENT_TYPE_FORM_DATA, + params: [ + // Should ignore value and send the file since type is set to file + {name: 'foo', fileName: fileName, value: 'bar', type: 'file'}, + + // Some extra params + {name: 'a', value: 'AA'}, + {name: 'baz', value: 'qux', disabled: true} + ] + } + }); + + const renderedRequest = await getRenderedRequest(request); + const {bodyBuffer} = await networkUtils._actuallySend( + renderedRequest, + workspace, + settings + ); + const body = JSON.parse(bodyBuffer); + expect(body.meta.READFUNCTION_VALUE).toBe([ + '--------------------------X-INSOMNIA-BOUNDARY', + 'Content-Disposition: form-data; name="foo"; filename="testfile.txt"', + 'Content-Type: text/plain', + '', + fs.readFileSync(fileName), + '--------------------------X-INSOMNIA-BOUNDARY', + 'Content-Disposition: form-data; name="a"', + '', + 'AA', + '--------------------------X-INSOMNIA-BOUNDARY--', + '' + ].join('\r\n')); + + expect(body.options).toEqual({ + POST: 1, + ACCEPT_ENCODING: '', + COOKIEFILE: '', + FOLLOWLOCATION: true, + MAXREDIRS: -1, + CUSTOMREQUEST: 'POST', + HTTPHEADER: [ + 'Content-Type: multipart/form-data; boundary=------------------------X-INSOMNIA-BOUNDARY', + 'Expect: ', + 'Transfer-Encoding: ' + ], + INFILESIZE_LARGE: 316, + NOPROGRESS: false, + PROXY: '', + TIMEOUT_MS: 0, + URL: 'http://localhost/', + UPLOAD: 1, + USERAGENT: `insomnia/${getAppVersion()}`, + VERBOSE: true + }); + }); it('uses unix socket', async () => { const workspace = await models.workspace.create(); diff --git a/app/network/multipart.js b/app/network/multipart.js index 21a1ad48c..6e146b09e 100644 --- a/app/network/multipart.js +++ b/app/network/multipart.js @@ -11,7 +11,7 @@ export function buildMultipart (params: Array): {boundary: const add = (v: Buffer | string) => { if (typeof v === 'string') { - buffers.push(Buffer.from(v, 'utf8')); + buffers.push(Buffer.from(v)); } else { buffers.push(v); } @@ -25,7 +25,7 @@ export function buildMultipart (params: Array): {boundary: continue; } - add(`${boundary}`); + add(`--${boundary}`); add(lineBreak); if (param.type === 'file' && param.fileName) { @@ -54,7 +54,7 @@ export function buildMultipart (params: Array): {boundary: add(lineBreak); } - add(`${boundary}--`); + add(`--${boundary}--`); add(lineBreak); const body = Buffer.concat(buffers); diff --git a/app/network/network.js b/app/network/network.js index 0b48739a3..83f9016de 100644 --- a/app/network/network.js +++ b/app/network/network.js @@ -15,9 +15,8 @@ import {join as pathJoin} from 'path'; import * as models from '../models'; import * as querystring from '../common/querystring'; import * as util from '../common/misc.js'; -import mimes from 'mime-types'; import {AUTH_AWS_IAM, AUTH_BASIC, AUTH_DIGEST, AUTH_NETRC, AUTH_NTLM, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, getAppVersion, STATUS_CODE_PLUGIN_ERROR} from '../common/constants'; -import {describeByteSize, hasAuthHeader, hasContentTypeHeader, hasUserAgentHeader, setDefaultProtocol} from '../common/misc'; +import {describeByteSize, getContentTypeHeader, hasAuthHeader, hasContentTypeHeader, hasUserAgentHeader, setDefaultProtocol} from '../common/misc'; import fs from 'fs'; import * as db from '../common/database'; import * as CACerts from './cacert'; @@ -27,6 +26,7 @@ import {getAuthHeader} from './authentication'; import {cookiesFromJar, jarFromCookies} from '../common/cookies'; import {urlMatchesCertHost} from './url-matches-cert-host'; import aws4 from 'aws4'; +import {buildMultipart} from './multipart'; export type ResponsePatch = { statusMessage?: string, @@ -389,15 +389,34 @@ export function _actuallySend ( requestBody = querystring.buildFromParams(renderedRequest.body.params || [], false); } else if (renderedRequest.body.mimeType === CONTENT_TYPE_FORM_DATA) { const params = renderedRequest.body.params || []; - const data = params.map(param => { - if (param.type === 'file' && param.fileName) { - const type = mimes.lookup(param.fileName) || 'application/octet-stream'; - return {name: param.name, file: param.fileName, type}; - } else { - return {name: param.name, contents: param.value}; + const {body: multipartBody, boundary} = buildMultipart(params); + + // Extend the Content-Type header + const contentTypeHeader = getContentTypeHeader(headers); + if (contentTypeHeader) { + contentTypeHeader.value = `multipart/form-data; boundary=${boundary}`; + } else { + headers.push({ + name: 'Content-Type', + value: `multipart/form-data; boundary=${boundary}` + }); + } + + setOpt(Curl.option.UPLOAD, 1); + setOpt(Curl.option.INFILESIZE_LARGE, multipartBody.length); + + // We need this, otherwise curl will send it as a PUT + setOpt(Curl.option.CUSTOMREQUEST, renderedRequest.method); + + let bytesUploaded = 0; + curl.setOpt(Curl.option.READFUNCTION, function (buffer, size, nmemb) { + if (bytesUploaded >= multipartBody.length || size === 0 || nmemb === 0) { + return 0; } + const wrote = multipartBody.copy(buffer, 0, bytesUploaded); + bytesUploaded += wrote; + return wrote; }); - setOpt(Curl.option.HTTPPOST, data); } else if (renderedRequest.body.fileName) { const {size} = fs.statSync(renderedRequest.body.fileName); const fileName = renderedRequest.body.fileName || ''; diff --git a/app/ui/components/pdf-viewer.js b/app/ui/components/pdf-viewer.js new file mode 100644 index 000000000..817856fa2 --- /dev/null +++ b/app/ui/components/pdf-viewer.js @@ -0,0 +1,112 @@ +// @flow +import * as React from 'react'; +import autobind from 'autobind-decorator'; +import PDF from 'pdfjs-dist/webpack'; + +type Props = { + body: Buffer, + uniqueKey: string +}; + +type State = { + numPages: number | null; +}; + +@autobind +class PDFViewer extends React.PureComponent { + container: ?HTMLDivElement; + debounceTimeout: any; + + setRef (n: ?HTMLDivElement) { + this.container = n; + } + + loadPDF () { + clearTimeout(this.debounceTimeout); + this.debounceTimeout = setTimeout(async () => { + // get node for this react component + const container = this.container; + + if (!container) { + return; + } + + container.innerHTML = ''; + + const containerWidth = container.clientWidth; + const pdf = await PDF.getDocument({data: this.props.body.toString('binary')}); + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const density = window.devicePixelRatio || 1; + + const {width: pdfWidth, height: pdfHeight} = page.getViewport(1); + const ratio = pdfHeight / pdfWidth; + const scale = containerWidth / pdfWidth; + const viewport = page.getViewport(scale * density); + + // set canvas for page + const canvas = document.createElement('canvas'); + + canvas.width = containerWidth * density; + // canvas.height = containerWidth * (viewport.height / viewport.width); + canvas.height = containerWidth * ratio * density; + canvas.style.width = `${containerWidth}px`; + canvas.style.height = `${containerWidth * ratio}px`; + + container.appendChild(canvas); + + // get context and render page + const context = canvas.getContext('2d'); + const renderContext = { + id: `${this.props.uniqueKey}.${i}`, + canvasContext: context, + viewport: viewport + }; + + page.render(renderContext); + } + }, 100); + } + + handleResize (e: SyntheticEvent) { + if (!this.container) { + return; + } + + clearTimeout(this.debounceTimeout); + this.debounceTimeout = setTimeout(this.loadPDF, 300); + } + + componentDidUpdate () { + this.loadPDF(); + } + + componentDidMount () { + this.loadPDF(); + window.addEventListener('resize', this.handleResize); + } + + componentWillUnmount () { + window.removeEventListener('resize', this.handleResize); + } + + render () { + const styles = { + width: '100%', + height: '100%', + overflowX: 'hidden', + overflowY: 'scroll', + padding: '0px' + }; + + return ( +
+
+ Loading PDF... +
+
+ ); + } +} + +export default PDFViewer; diff --git a/app/ui/components/response-pane.js b/app/ui/components/response-pane.js index 8cde30619..3c716a10f 100644 --- a/app/ui/components/response-pane.js +++ b/app/ui/components/response-pane.js @@ -281,6 +281,7 @@ class ResponsePane extends React.PureComponent { ); } else if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('application/pdf') === 0) { - const justContentType = contentType.split(';')[0]; - const base64Body = bodyBuffer.toString('base64'); return (
- +
); } else if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('audio/') === 0) { @@ -277,6 +276,7 @@ class ResponseViewer extends React.Component { ResponseViewer.propTypes = { getBody: PropTypes.func.isRequired, + responseId: PropTypes.string.isRequired, previewMode: PropTypes.string.isRequired, filter: PropTypes.string.isRequired, filterHistory: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, diff --git a/flow-typed/form-data.js b/flow-typed/form-data.js deleted file mode 100644 index 5a0869031..000000000 --- a/flow-typed/form-data.js +++ /dev/null @@ -1,3 +0,0 @@ -declare module 'form-data/lib/form_data' { - declare module.exports: * -} diff --git a/flow-typed/pdfjs-dist.js b/flow-typed/pdfjs-dist.js new file mode 100644 index 000000000..d21da756d --- /dev/null +++ b/flow-typed/pdfjs-dist.js @@ -0,0 +1,3 @@ +declare module 'pdfjs-dist/webpack' { + declare module.exports: * +} diff --git a/package-lock.json b/package-lock.json index 364160865..13c8611c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2421,16 +2421,6 @@ "sha.js": "2.4.8" } }, - "create-react-class": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.2.tgz", - "integrity": "sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=", - "requires": { - "fbjs": "0.8.12", - "loose-envify": "1.3.1", - "object-assign": "4.1.1" - } - }, "cross-env": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-2.0.0.tgz", @@ -9449,12 +9439,12 @@ "dev": true }, "pdfjs-dist": { - "version": "1.8.618", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-1.8.618.tgz", - "integrity": "sha1-/Cx235u3SZafqj79ARWqrIcd0fE=", + "version": "1.9.640", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-1.9.640.tgz", + "integrity": "sha1-8EOFg2R9d2nGGXHE+cfi+m4Qdjo=", "requires": { "node-ensure": "0.0.0", - "worker-loader": "0.8.1" + "worker-loader": "1.0.0" } }, "pend": { @@ -11134,13 +11124,13 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", "requires": { - "ajv": "5.2.2" + "ajv": "5.2.3" }, "dependencies": { "ajv": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.2.tgz", - "integrity": "sha1-R8aNaehvXZUxA7AHSpQw3GPaXjk=", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz", + "integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=", "requires": { "co": "4.6.0", "fast-deep-equal": "1.0.0", @@ -11330,41 +11320,6 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, - "simple-react-pdf": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/simple-react-pdf/-/simple-react-pdf-1.0.8.tgz", - "integrity": "sha1-QtlNWMyxxSJ84snpISuWiE2fItQ=", - "requires": { - "pdfjs-dist": "1.8.618", - "react": "15.6.2", - "react-dom": "15.6.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "15.6.2", - "fbjs": "0.8.12", - "loose-envify": "1.3.1", - "object-assign": "4.1.1", - "prop-types": "15.5.10" - } - }, - "react-dom": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", - "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", - "requires": { - "fbjs": "0.8.12", - "loose-envify": "1.3.1", - "object-assign": "4.1.1", - "prop-types": "15.5.10" - } - } - } - }, "single-line-log": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz", @@ -13394,9 +13349,9 @@ } }, "worker-loader": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-0.8.1.tgz", - "integrity": "sha1-6OmVMx6jTfW/aCloJL+38K1XjUM=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-1.0.0.tgz", + "integrity": "sha512-dUwgs4Rdi1qG3VciM1+EPgAoO8m9USpCXxE3xmpWrnHJSMKGkzpCUNeYLjBRgYcSkf2A5xnXpR450Wqtu+pq0w==", "requires": { "loader-utils": "1.1.0", "schema-utils": "0.3.0" diff --git a/package.json b/package.json index a6d7bdc82..c6e611407 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "./__jest__/setup.js" ], "moduleNameMapper": { - "\\.(css|less|png)$": "/__mocks__/dummy.js" + "\\.(css|less|png)$": "/__mocks__/dummy.js", + "^worker-loader!": "/__mocks__/dummy.js" }, "testMatch": [ "**/__tests__/**/*.test.js?(x)" @@ -142,6 +143,7 @@ "nedb": "^1.8.0", "node-forge": "^0.7.0", "nunjucks": "^3.0.0", + "pdfjs-dist": "^1.9.640", "prop-types": "^15.5.10", "react": "^16.0.0", "react-dnd": "^2.4.0", @@ -152,7 +154,6 @@ "redux": "^3.7.2", "redux-thunk": "^2.2.0", "reselect": "^3.0.1", - "simple-react-pdf": "^1.0.8", "srp-js": "^0.2.0", "tar": "^3.1.7", "tough-cookie": "^2.3.1",