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:
authored and GitHub committed 2021-09-09 15:02:42 +00:00
1 parent 614957c2b2
commit d4da3ad9d4
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 = '&lt;a href=&quot;http://example.com?query1=value1&amp;query2=value2&quot;&gt;a link&lt;/a&gt;';
const output = '<a href="http://example.com?query1=value1&query2=value2">a link</a>';
expect(xmlDecode(input)).toEqual(output);
});
});
+13
View File
@@ -200,6 +200,19 @@ export function preventDefault(e: Event) {
e.preventDefault();
}
export function xmlDecode(input: string) {
const ESCAPED_CHARACTERS_MAP = {
'&amp;': '&',
'&quot;': '"',
'&lt;': '<',
'&gt;': '>',
};
return input.replace(/(&quot;|&lt;|&gt;|&amp;)/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() {