Rewrote broken PDF viewer

This commit is contained in:
Gregory Schier 2017-10-12 19:32:26 +02:00
parent 6ca5204961
commit 0d56b84c21
13 changed files with 247 additions and 162 deletions

View File

@ -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();
});
});

View File

@ -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);

View File

@ -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'));
});

View File

@ -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();

View File

@ -11,7 +11,7 @@ export function buildMultipart (params: Array<RequestBodyParameter>): {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<RequestBodyParameter>): {boundary:
continue;
}
add(`${boundary}`);
add(`--${boundary}`);
add(lineBreak);
if (param.type === 'file' && param.fileName) {
@ -54,7 +54,7 @@ export function buildMultipart (params: Array<RequestBodyParameter>): {boundary:
add(lineBreak);
}
add(`${boundary}--`);
add(`--${boundary}--`);
add(lineBreak);
const body = Buffer.concat(buffers);

View File

@ -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};
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 {
return {name: param.name, contents: param.value};
}
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 || '';

View File

@ -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<Props, State> {
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<HTMLDivElement>) {
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 (
<div className="S-PDF-ID" ref={this.setRef} style={styles}>
<div className="faded text-center vertically-center tall">
Loading PDF...
</div>
</div>
);
}
}
export default PDFViewer;

View File

@ -281,6 +281,7 @@ class ResponsePane extends React.PureComponent<Props> {
<ResponseViewer
key={response._id}
// Send larger one because legacy responses have bytesContent === -1
responseId={response._id}
bytes={Math.max(response.bytesContent, response.bytesRead)}
contentType={response.contentType || ''}
previewMode={response.error ? PREVIEW_MODE_SOURCE : previewMode}

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import iconv from 'iconv-lite';
import autobind from 'autobind-decorator';
import {shell} from 'electron';
import {SimplePDF} from 'simple-react-pdf';
import PDFViewer from '../pdf-viewer';
import CodeEditor from '../codemirror/code-editor';
import ResponseWebView from './response-webview';
import ResponseRaw from './response-raw';
@ -113,6 +113,7 @@ class ResponseViewer extends React.Component {
editorIndentSize,
editorKeyMap,
updateFilter,
responseId,
url,
error
} = this.props;
@ -214,11 +215,9 @@ class ResponseViewer extends React.Component {
/>
);
} else if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('application/pdf') === 0) {
const justContentType = contentType.split(';')[0];
const base64Body = bodyBuffer.toString('base64');
return (
<div className="tall wide scrollable">
<SimplePDF file={`data:${justContentType};base64,${base64Body}`}/>
<PDFViewer body={bodyBuffer} uniqueKey={responseId}/>
</div>
);
} 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,

View File

@ -1,3 +0,0 @@
declare module 'form-data/lib/form_data' {
declare module.exports: *
}

3
flow-typed/pdfjs-dist.js vendored Normal file
View File

@ -0,0 +1,3 @@
declare module 'pdfjs-dist/webpack' {
declare module.exports: *
}

67
package-lock.json generated
View File

@ -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"

View File

@ -43,7 +43,8 @@
"./__jest__/setup.js"
],
"moduleNameMapper": {
"\\.(css|less|png)$": "<rootDir>/__mocks__/dummy.js"
"\\.(css|less|png)$": "<rootDir>/__mocks__/dummy.js",
"^worker-loader!": "<rootDir>/__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",