mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-24 23:59:16 -04:00
Unescape xml links before opening it (#2725)
Co-authored-by: James Gatz <jamesgatzos@gmail.com> Co-authored-by: Dimitri Mitropoulos <dimitrimitropoulos@gmail.com>
This commit is contained in:
4 files changed
+154
-95
No files matched your search
@@ -1,18 +1,26 @@
|
||||
import { globalBeforeEach } from '../../__jest__/before-each';
|
||||
import * as misc from '../misc';
|
||||
import {
|
||||
chunkArray,
|
||||
convertEpochToMilliseconds,
|
||||
debounce,
|
||||
diffPatchObj,
|
||||
filterHeaders,
|
||||
fuzzyMatch,
|
||||
fuzzyMatchAll,
|
||||
generateId,
|
||||
hasAuthHeader,
|
||||
isNotNullOrUndefined,
|
||||
keyedDebounce,
|
||||
pluralize,
|
||||
snapNumberToLimits,
|
||||
xmlDecode,
|
||||
} from '../misc';
|
||||
|
||||
describe('hasAuthHeader()', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
it('finds valid header', () => {
|
||||
const yes = misc.hasAuthHeader([
|
||||
const yes = hasAuthHeader([
|
||||
{
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
@@ -26,7 +34,7 @@ describe('hasAuthHeader()', () => {
|
||||
});
|
||||
|
||||
it('finds valid header case insensitive', () => {
|
||||
const yes = misc.hasAuthHeader([
|
||||
const yes = hasAuthHeader([
|
||||
{
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
@@ -44,12 +52,12 @@ describe('generateId()', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
it('generates a valid ID', () => {
|
||||
const id = misc.generateId('foo');
|
||||
const id = generateId('foo');
|
||||
expect(id).toMatch(/^foo_[a-z0-9]{32}$/);
|
||||
});
|
||||
|
||||
it('generates without prefix', () => {
|
||||
const id = misc.generateId();
|
||||
const id = generateId();
|
||||
expect(id).toMatch(/^[a-z0-9]{32}$/);
|
||||
});
|
||||
});
|
||||
@@ -58,13 +66,13 @@ describe('filterHeaders()', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
it('handles bad headers', () => {
|
||||
expect(misc.filterHeaders(null, null)).toEqual([]);
|
||||
expect(misc.filterHeaders([], null)).toEqual([]);
|
||||
expect(misc.filterHeaders(['bad'], null)).toEqual([]);
|
||||
expect(misc.filterHeaders(['bad'], 'good')).toEqual([]);
|
||||
expect(misc.filterHeaders(null, 'good')).toEqual([]);
|
||||
expect(filterHeaders(null, null)).toEqual([]);
|
||||
expect(filterHeaders([], null)).toEqual([]);
|
||||
expect(filterHeaders(['bad'], null)).toEqual([]);
|
||||
expect(filterHeaders(['bad'], 'good')).toEqual([]);
|
||||
expect(filterHeaders(null, 'good')).toEqual([]);
|
||||
expect(
|
||||
misc.filterHeaders(
|
||||
filterHeaders(
|
||||
[
|
||||
{
|
||||
name: '',
|
||||
@@ -75,7 +83,7 @@ describe('filterHeaders()', () => {
|
||||
),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
misc.filterHeaders(
|
||||
filterHeaders(
|
||||
[
|
||||
{
|
||||
name: 123,
|
||||
@@ -86,7 +94,7 @@ describe('filterHeaders()', () => {
|
||||
),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
misc.filterHeaders(
|
||||
filterHeaders(
|
||||
[
|
||||
{
|
||||
name: 'good',
|
||||
@@ -97,7 +105,7 @@ describe('filterHeaders()', () => {
|
||||
),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
misc.filterHeaders(
|
||||
filterHeaders(
|
||||
[
|
||||
{
|
||||
name: 'good',
|
||||
@@ -108,7 +116,7 @@ describe('filterHeaders()', () => {
|
||||
),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
misc.filterHeaders(
|
||||
filterHeaders(
|
||||
[
|
||||
{
|
||||
name: 'good',
|
||||
@@ -134,7 +142,7 @@ describe('keyedDebounce()', () => {
|
||||
|
||||
it('debounces correctly', () => {
|
||||
const resultsList = [];
|
||||
const fn = misc.keyedDebounce(results => {
|
||||
const fn = keyedDebounce(results => {
|
||||
resultsList.push(results);
|
||||
}, 100);
|
||||
fn('foo', 'bar');
|
||||
@@ -163,7 +171,7 @@ describe('debounce()', () => {
|
||||
|
||||
it('debounces correctly', () => {
|
||||
const resultList = [];
|
||||
const fn = misc.debounce((...args) => {
|
||||
const fn = debounce((...args) => {
|
||||
resultList.push(args);
|
||||
}, 100);
|
||||
fn('foo');
|
||||
@@ -182,12 +190,12 @@ describe('fuzzyMatch()', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
it('can get a positive fuzzy match on a single field', () => {
|
||||
expect(misc.fuzzyMatch('test', 'testing')).toEqual({
|
||||
expect(fuzzyMatch('test', 'testing')).toEqual({
|
||||
score: -3,
|
||||
indexes: [0, 1, 2, 3],
|
||||
target: 'testing',
|
||||
});
|
||||
expect(misc.fuzzyMatch('tst', 'testing')).toEqual({
|
||||
expect(fuzzyMatch('tst', 'testing')).toEqual({
|
||||
score: -2004,
|
||||
indexes: [0, 2, 3],
|
||||
target: 'testing',
|
||||
@@ -195,8 +203,8 @@ describe('fuzzyMatch()', () => {
|
||||
});
|
||||
|
||||
it('can get a negative fuzzy match on a single field', () => {
|
||||
expect(misc.fuzzyMatch('foo', undefined)).toBeNull();
|
||||
expect(misc.fuzzyMatch('foo', 'bar')).toBeNull();
|
||||
expect(fuzzyMatch('foo', undefined)).toBeNull();
|
||||
expect(fuzzyMatch('foo', 'bar')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -204,16 +212,16 @@ describe('fuzzyMatchAll()', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
it('can get a positive fuzzy match on multiple fields', () => {
|
||||
expect(misc.fuzzyMatchAll('', [undefined])).toEqual(null);
|
||||
expect(misc.fuzzyMatchAll('', ['testing'])).toEqual(null);
|
||||
expect(misc.fuzzyMatchAll(' ', ['testing'])).toEqual(null);
|
||||
expect(misc.fuzzyMatchAll('test', ['testing', 'foo'])).toEqual({
|
||||
expect(fuzzyMatchAll('', [undefined])).toEqual(null);
|
||||
expect(fuzzyMatchAll('', ['testing'])).toEqual(null);
|
||||
expect(fuzzyMatchAll(' ', ['testing'])).toEqual(null);
|
||||
expect(fuzzyMatchAll('test', ['testing', 'foo'])).toEqual({
|
||||
score: -3,
|
||||
indexes: [0, 1, 2, 3],
|
||||
target: 'testing foo',
|
||||
});
|
||||
expect(
|
||||
misc.fuzzyMatchAll('test foo', ['testing', 'foo'], {
|
||||
fuzzyMatchAll('test foo', ['testing', 'foo'], {
|
||||
splitSpace: true,
|
||||
}),
|
||||
).toEqual({
|
||||
@@ -221,13 +229,13 @@ describe('fuzzyMatchAll()', () => {
|
||||
indexes: [0, 1, 2, 3, 0, 1, 2],
|
||||
target: 'testing foo',
|
||||
});
|
||||
expect(misc.fuzzyMatchAll('tst', ['testing'])).toEqual({
|
||||
expect(fuzzyMatchAll('tst', ['testing'])).toEqual({
|
||||
score: -2004,
|
||||
indexes: [0, 2, 3],
|
||||
target: 'testing',
|
||||
});
|
||||
expect(
|
||||
misc.fuzzyMatch('tst this ou', 'testing this out', {
|
||||
fuzzyMatch('tst this ou', 'testing this out', {
|
||||
splitSpace: true,
|
||||
loose: true,
|
||||
}),
|
||||
@@ -239,15 +247,15 @@ describe('fuzzyMatchAll()', () => {
|
||||
});
|
||||
|
||||
it('can get a negative fuzzy match on multiple fields', () => {
|
||||
expect(misc.fuzzyMatchAll('foo', [undefined])).toEqual(null);
|
||||
expect(misc.fuzzyMatchAll('foo', ['bar'])).toEqual(null);
|
||||
expect(misc.fuzzyMatchAll('wrong this ou', ['testing', 'this', 'out'])).toEqual(null);
|
||||
expect(fuzzyMatchAll('foo', [undefined])).toEqual(null);
|
||||
expect(fuzzyMatchAll('foo', ['bar'])).toEqual(null);
|
||||
expect(fuzzyMatchAll('wrong this ou', ['testing', 'this', 'out'])).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chunkArray()', () => {
|
||||
it('works with exact divisor', () => {
|
||||
const chunks = misc.chunkArray([1, 2, 3, 4, 5, 6], 3);
|
||||
const chunks = chunkArray([1, 2, 3, 4, 5, 6], 3);
|
||||
expect(chunks).toEqual([
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
@@ -255,7 +263,7 @@ describe('chunkArray()', () => {
|
||||
});
|
||||
|
||||
it('works with weird divisor', () => {
|
||||
const chunks = misc.chunkArray([1, 2, 3, 4, 5, 6], 4);
|
||||
const chunks = chunkArray([1, 2, 3, 4, 5, 6], 4);
|
||||
expect(chunks).toEqual([
|
||||
[1, 2, 3, 4],
|
||||
[5, 6],
|
||||
@@ -263,12 +271,12 @@ describe('chunkArray()', () => {
|
||||
});
|
||||
|
||||
it('works with empty', () => {
|
||||
const chunks = misc.chunkArray([], 4);
|
||||
const chunks = chunkArray([], 4);
|
||||
expect(chunks).toEqual([]);
|
||||
});
|
||||
|
||||
it('works with less than one chunk', () => {
|
||||
const chunks = misc.chunkArray([1, 2], 4);
|
||||
const chunks = chunkArray([1, 2], 4);
|
||||
expect(chunks).toEqual([[1, 2]]);
|
||||
});
|
||||
});
|
||||
@@ -397,3 +405,11 @@ describe('isNotNullOrUndefined', () => {
|
||||
expect(isNotNullOrUndefined(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('xmlDecode()', () => {
|
||||
it('unescape characters', () => {
|
||||
const input = '<a href="http://example.com?query1=value1&query2=value2">a link</a>';
|
||||
const output = '<a href="http://example.com?query1=value1&query2=value2">a link</a>';
|
||||
expect(xmlDecode(input)).toEqual(output);
|
||||
});
|
||||
});
|
||||
@@ -200,6 +200,19 @@ export function preventDefault(e: Event) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
export function xmlDecode(input: string) {
|
||||
const ESCAPED_CHARACTERS_MAP = {
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
};
|
||||
|
||||
return input.replace(/("|<|>|&)/g, (_: string, item: keyof typeof ESCAPED_CHARACTERS_MAP) => (
|
||||
ESCAPED_CHARACTERS_MAP[item])
|
||||
);
|
||||
}
|
||||
|
||||
export function fnOrString(v: string | ((...args: any[]) => any), ...args: any[]) {
|
||||
if (typeof v === 'string') {
|
||||
return v;
|
||||
|
||||
@@ -169,6 +169,7 @@ class KeyValueEditorRow extends PureComponent<Props, State> {
|
||||
|
||||
_handleTypeChange(def) {
|
||||
// Remove newlines if converting to text
|
||||
// WARNING: props should never be overwritten!
|
||||
let value = this.props.pair.value || '';
|
||||
|
||||
if (def.type === 'text' && !def.multiline && value.includes('\n')) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { clickLink } from '../../../common/electron-helpers';
|
||||
import { hotKeyRefs } from '../../../common/hotkeys';
|
||||
import { executeHotKey } from '../../../common/hotkeys-listener';
|
||||
import { xmlDecode } from '../../../common/misc';
|
||||
import CodeEditor from '../codemirror/code-editor';
|
||||
import KeydownBinder from '../keydown-binder';
|
||||
import CSVViewer from './response-csv-viewer';
|
||||
@@ -71,15 +72,6 @@ class ResponseViewer extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
_decodeIconv(bodyBuffer: Buffer, charset: string) {
|
||||
try {
|
||||
return iconv.decode(bodyBuffer, charset);
|
||||
} catch (err) {
|
||||
console.warn('[response] Failed to decode body', err);
|
||||
return bodyBuffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
_handleDismissBlocker() {
|
||||
this.setState({
|
||||
blockingBecauseTooLarge: false,
|
||||
@@ -205,6 +197,49 @@ class ResponseViewer extends Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
_getBody() {
|
||||
const { bodyBuffer } = this.state;
|
||||
const { contentType } = this.props;
|
||||
|
||||
if (!bodyBuffer) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Show everything else as "source"
|
||||
const match = contentType.match(/charset=([\w-]+)/);
|
||||
const charset = match && match.length >= 2 ? match[1] : 'utf-8';
|
||||
|
||||
try {
|
||||
return iconv.decode(bodyBuffer, charset);
|
||||
} catch (err) {
|
||||
console.warn('[response] Failed to decode body', err);
|
||||
// Sometimes iconv conversion fails so fallback to regular buffer
|
||||
return bodyBuffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/** Try to detect content-types if there isn't one */
|
||||
_getMode() {
|
||||
const { contentType } = this.props;
|
||||
const body = this._getBody();
|
||||
if (body?.match(/^\s*<\?xml [^?]*\?>/)) {
|
||||
return 'application/xml';
|
||||
} else {
|
||||
return contentType;
|
||||
}
|
||||
}
|
||||
|
||||
_handleClickLink(url: string) {
|
||||
const mode = this._getMode();
|
||||
|
||||
if (mode === 'application/xml') {
|
||||
clickLink(xmlDecode(url));
|
||||
return;
|
||||
}
|
||||
|
||||
clickLink(url);
|
||||
}
|
||||
|
||||
_renderView() {
|
||||
const {
|
||||
disableHtmlPreviewJs,
|
||||
@@ -222,7 +257,8 @@ class ResponseViewer extends Component<Props, State> {
|
||||
updateFilter,
|
||||
url,
|
||||
} = this.props;
|
||||
let contentType = this.props.contentType;
|
||||
// WARNING: props should never be overwritten!
|
||||
let { contentType } = this.props;
|
||||
const { bodyBuffer, error: parseError } = this.state;
|
||||
const error = responseError || parseError;
|
||||
|
||||
@@ -331,31 +367,37 @@ class ResponseViewer extends Component<Props, State> {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (previewMode === PREVIEW_MODE_FRIENDLY && ct.includes('html')) {
|
||||
const match = contentType.match(/charset=([\w-]+)/);
|
||||
const charset = match && match.length >= 2 ? match[1] : 'utf-8';
|
||||
}
|
||||
|
||||
if (previewMode === PREVIEW_MODE_FRIENDLY && ct.includes('html')) {
|
||||
return (
|
||||
<ResponseWebView
|
||||
body={this._decodeIconv(bodyBuffer, charset)}
|
||||
body={this._getBody()}
|
||||
contentType={contentType}
|
||||
key={disableHtmlPreviewJs ? 'no-js' : 'yes-js'}
|
||||
url={url}
|
||||
webpreferences={disableHtmlPreviewJs ? 'javascript=no' : 'javascript=yes'}
|
||||
/>
|
||||
);
|
||||
} else if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('application/pdf') === 0) {
|
||||
}
|
||||
|
||||
if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('application/pdf') === 0) {
|
||||
return (
|
||||
<div className="tall wide scrollable">
|
||||
<PDFViewer body={bodyBuffer} uniqueKey={responseId} />
|
||||
</div>
|
||||
);
|
||||
} else if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('text/csv') === 0) {
|
||||
}
|
||||
|
||||
if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('text/csv') === 0) {
|
||||
return (
|
||||
<div className="tall wide scrollable">
|
||||
<CSVViewer body={bodyBuffer} key={responseId} />
|
||||
</div>
|
||||
);
|
||||
} else if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('multipart/') === 0) {
|
||||
}
|
||||
|
||||
if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('multipart/') === 0) {
|
||||
return (
|
||||
<MultipartViewer
|
||||
bodyBuffer={bodyBuffer}
|
||||
@@ -374,7 +416,9 @@ class ResponseViewer extends Component<Props, State> {
|
||||
url={url}
|
||||
/>
|
||||
);
|
||||
} else if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('audio/') === 0) {
|
||||
}
|
||||
|
||||
if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('audio/') === 0) {
|
||||
const justContentType = contentType.split(';')[0];
|
||||
const base64Body = bodyBuffer.toString('base64');
|
||||
return (
|
||||
@@ -384,57 +428,42 @@ class ResponseViewer extends Component<Props, State> {
|
||||
</audio>
|
||||
</div>
|
||||
);
|
||||
} else if (previewMode === PREVIEW_MODE_RAW) {
|
||||
const match = contentType.match(/charset=([\w-]+)/);
|
||||
const charset = match && match.length >= 2 ? match[1] : 'utf-8';
|
||||
}
|
||||
|
||||
if (previewMode === PREVIEW_MODE_RAW) {
|
||||
return (
|
||||
<ResponseRaw
|
||||
key={responseId}
|
||||
responseId={responseId}
|
||||
ref={this._setSelectableViewRef}
|
||||
value={this._decodeIconv(bodyBuffer, charset)}
|
||||
value={this._getBody()}
|
||||
fontSize={editorFontSize}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// Show everything else as "source"
|
||||
const match = contentType.match(/charset=([\w-]+)/);
|
||||
const charset = match && match.length >= 2 ? match[1] : 'utf-8';
|
||||
|
||||
// Sometimes iconv conversion fails so fallback to regular buffer
|
||||
const body = this._decodeIconv(bodyBuffer, charset);
|
||||
|
||||
// Try to detect content-types if there isn't one
|
||||
let mode;
|
||||
|
||||
if (!mode && body.match(/^\s*<\?xml [^?]*\?>/)) {
|
||||
mode = 'application/xml';
|
||||
} else {
|
||||
mode = contentType;
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
key={disablePreviewLinks ? 'links-no' : 'links-yes'}
|
||||
ref={this._setSelectableViewRef}
|
||||
autoPrettify
|
||||
defaultValue={body}
|
||||
filter={filter}
|
||||
filterHistory={filterHistory}
|
||||
fontSize={editorFontSize}
|
||||
indentSize={editorIndentSize}
|
||||
keyMap={editorKeyMap}
|
||||
lineWrapping={editorLineWrapping}
|
||||
mode={mode}
|
||||
noMatchBrackets
|
||||
onClickLink={disablePreviewLinks ? undefined : clickLink}
|
||||
placeholder="..."
|
||||
readOnly
|
||||
uniquenessKey={responseId}
|
||||
updateFilter={updateFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Show everything else as "source"
|
||||
return (
|
||||
<CodeEditor
|
||||
key={disablePreviewLinks ? 'links-disabled' : 'links-enabled'}
|
||||
ref={this._setSelectableViewRef}
|
||||
autoPrettify
|
||||
defaultValue={this._getBody()}
|
||||
filter={filter}
|
||||
filterHistory={filterHistory}
|
||||
fontSize={editorFontSize}
|
||||
indentSize={editorIndentSize}
|
||||
keyMap={editorKeyMap}
|
||||
lineWrapping={editorLineWrapping}
|
||||
mode={this._getMode()}
|
||||
noMatchBrackets
|
||||
onClickLink={disablePreviewLinks ? undefined : this._handleClickLink}
|
||||
placeholder="..."
|
||||
readOnly
|
||||
uniquenessKey={responseId}
|
||||
updateFilter={updateFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
Reference in new issue
Block a user