From d4da3ad9d4d3231dd8a969e41822a3a3fbfc123f Mon Sep 17 00:00:00 2001 From: Julien Giovaresco Date: Thu, 9 Sep 2021 17:02:42 +0200 Subject: [PATCH] Unescape xml links before opening it (#2725) Co-authored-by: James Gatz Co-authored-by: Dimitri Mitropoulos --- .../app/common/__tests__/misc.test.ts | 86 ++++++---- packages/insomnia-app/app/common/misc.ts | 13 ++ .../ui/components/key-value-editor/row.tsx | 1 + .../ui/components/viewers/response-viewer.tsx | 149 +++++++++++------- 4 files changed, 154 insertions(+), 95 deletions(-) diff --git a/packages/insomnia-app/app/common/__tests__/misc.test.ts b/packages/insomnia-app/app/common/__tests__/misc.test.ts index 4f9739d8d3..a65defae10 100644 --- a/packages/insomnia-app/app/common/__tests__/misc.test.ts +++ b/packages/insomnia-app/app/common/__tests__/misc.test.ts @@ -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 link'; + expect(xmlDecode(input)).toEqual(output); + }); +}); diff --git a/packages/insomnia-app/app/common/misc.ts b/packages/insomnia-app/app/common/misc.ts index 3bc584fe1b..46f8157ec1 100644 --- a/packages/insomnia-app/app/common/misc.ts +++ b/packages/insomnia-app/app/common/misc.ts @@ -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; diff --git a/packages/insomnia-app/app/ui/components/key-value-editor/row.tsx b/packages/insomnia-app/app/ui/components/key-value-editor/row.tsx index aba94c99d1..7ba317e9a1 100644 --- a/packages/insomnia-app/app/ui/components/key-value-editor/row.tsx +++ b/packages/insomnia-app/app/ui/components/key-value-editor/row.tsx @@ -169,6 +169,7 @@ class KeyValueEditorRow extends PureComponent { _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')) { diff --git a/packages/insomnia-app/app/ui/components/viewers/response-viewer.tsx b/packages/insomnia-app/app/ui/components/viewers/response-viewer.tsx index 724b9fc73d..e2406ca6b9 100644 --- a/packages/insomnia-app/app/ui/components/viewers/response-viewer.tsx +++ b/packages/insomnia-app/app/ui/components/viewers/response-viewer.tsx @@ -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 { } } - _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 { }); } + _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 { 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 { ); - } 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 ( ); - } else if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('application/pdf') === 0) { + } + + if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('application/pdf') === 0) { return (
); - } else if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('text/csv') === 0) { + } + + if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('text/csv') === 0) { return (
); - } else if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('multipart/') === 0) { + } + + if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('multipart/') === 0) { return ( { 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 { ); - } 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 ( ); - } 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 ( - - ); } + + // Show everything else as "source" + return ( + + ); } render() {