Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

URL encoding for URL drilldown #86902

Merged
merged 9 commits into from
Jan 4, 2021
6 changes: 6 additions & 0 deletions docs/user/dashboard/url-drilldown.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ Example:

`{{split event.value ","}}`

|encodeURIComponent
a|Escapes string using built in `encodeURIComponent` function.

|encodeURIQuery
a|Escapes string using built in `encodeURIComponent` function, while keeping "@", ":", "$", ",", and ";" characters as is.

|===


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,77 @@ describe('UrlDrilldown', () => {
});
});
});

describe('encoding', () => {
const urlDrilldown = createDrilldown();
const context: ActionContext = {
data: {
data: mockDataPoints,
},
embeddable: mockEmbeddable,
};

test('encodes URL by default', async () => {
const config: Config = {
url: {
template: 'https://elastic.co?foo=head%26shoulders',
},
openInNewTab: false,
};
const url = await urlDrilldown.getHref(config, context);

expect(url).toBe('https://elastic.co?foo=head%2526shoulders');
});

test('encodes URL when encoding is enabled', async () => {
const config: Config = {
url: {
template: 'https://elastic.co?foo=head%26shoulders',
},
openInNewTab: false,
encodeUrl: true,
};
const url = await urlDrilldown.getHref(config, context);

expect(url).toBe('https://elastic.co?foo=head%2526shoulders');
});

test('does not encode URL when encoding is not enabled', async () => {
const config: Config = {
url: {
template: 'https://elastic.co?foo=head%26shoulders',
},
openInNewTab: false,
encodeUrl: false,
};
const url = await urlDrilldown.getHref(config, context);

expect(url).toBe('https://elastic.co?foo=head%26shoulders');
});

test('can encode URI component using "encodeURIComponent" Handlebars helper', async () => {
const config: Config = {
url: {
template: 'https://elastic.co?foo={{encodeURIComponent "head%26shoulders@gmail.com"}}',
},
openInNewTab: false,
encodeUrl: false,
};
const url = await urlDrilldown.getHref(config, context);

expect(url).toBe('https://elastic.co?foo=head%2526shoulders%40gmail.com');
});

test('can encode URI component using "encodeURIQuery" Handlebars helper', async () => {
const config: Config = {
url: {
template: 'https://elastic.co?foo={{encodeURIQuery "head%26shoulders@gmail.com"}}',
},
openInNewTab: false,
encodeUrl: false,
};
const url = await urlDrilldown.getHref(config, context);

expect(url).toBe('https://elastic.co?foo=head%2526shoulders@gmail.com');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory

public readonly createConfig = () => ({
url: { template: '' },
openInNewTab: false,
openInNewTab: true,
encodeUrl: true,
});

public readonly isConfigValid = (config: Config): config is Config => {
Expand Down Expand Up @@ -133,7 +134,12 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory
};

private buildUrl(config: Config, context: ActionContext): string {
const url = urlDrilldownCompileUrl(config.url.template, this.getRuntimeVariables(context));
const doEncode = config.encodeUrl ?? true;
const url = urlDrilldownCompileUrl(
config.url.template,
this.getRuntimeVariables(context),
doEncode
);
return url;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const txtAddVariableButtonTitle = i18n.translate(
export const txtUrlTemplateLabel = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel',
{
defaultMessage: 'Enter URL template:',
defaultMessage: 'Enter URL:',
}
);

Expand Down Expand Up @@ -76,6 +76,27 @@ export const txtUrlTemplatePreviewLinkText = i18n.translate(
export const txtUrlTemplateOpenInNewTab = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel',
{
defaultMessage: 'Open in new tab',
defaultMessage: 'Open in new window',
}
);

export const txtUrlTemplateAdditionalOptions = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.additionalOptions',
{
defaultMessage: 'Additional options',
}
);

export const txtUrlTemplateEncodeUrl = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeUrl',
{
defaultMessage: 'Encode URL',
}
);

export const txtUrlTemplateEncodeDescription = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeDescription',
{
defaultMessage: 'If enabled, URL will be escaped using percent encoding',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import React, { useRef, useState } from 'react';
import {
EuiCheckbox,
EuiFormRow,
EuiIcon,
EuiLink,
Expand All @@ -17,6 +16,11 @@ import {
EuiText,
EuiTextArea,
EuiSelectableOption,
EuiSwitch,
EuiAccordion,
EuiSpacer,
EuiPanel,
EuiTextColor,
} from '@elastic/eui';
import { UrlDrilldownConfig } from '../../types';
import './index.scss';
Expand All @@ -28,6 +32,9 @@ import {
txtUrlTemplateLabel,
txtUrlTemplateOpenInNewTab,
txtUrlTemplatePlaceholder,
txtUrlTemplateAdditionalOptions,
txtUrlTemplateEncodeUrl,
txtUrlTemplateEncodeDescription,
} from './i18n';

export interface UrlDrilldownCollectConfig {
Expand Down Expand Up @@ -110,15 +117,39 @@ export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfig> = ({
inputRef={textAreaRef}
/>
</EuiFormRow>
<EuiFormRow hasChildLabel={false}>
<EuiCheckbox
id="openInNewTab"
name="openInNewTab"
label={txtUrlTemplateOpenInNewTab}
checked={config.openInNewTab}
onChange={() => onConfig({ ...config, openInNewTab: !config.openInNewTab })}
/>
</EuiFormRow>
<EuiSpacer size={'l'} />
<EuiAccordion
id="accordion_url_drilldown_additional_options"
buttonContent={txtUrlTemplateAdditionalOptions}
>
<EuiSpacer size={'s'} />
<EuiPanel color="subdued" borderRadius="none" hasShadow={false} style={{ border: 'none' }}>
<EuiFormRow hasChildLabel={false}>
<EuiSwitch
id="openInNewTab"
name="openInNewTab"
label={txtUrlTemplateOpenInNewTab}
checked={config.openInNewTab}
onChange={() => onConfig({ ...config, openInNewTab: !config.openInNewTab })}
/>
</EuiFormRow>
<EuiFormRow hasChildLabel={false} fullWidth>
<EuiSwitch
id="encodeUrl"
name="encodeUrl"
label={
<>
{txtUrlTemplateEncodeUrl}
<EuiSpacer size={'s'} />
<EuiTextColor color="subdued">{txtUrlTemplateEncodeDescription}</EuiTextColor>
</>
}
checked={config.encodeUrl ?? true}
onChange={() => onConfig({ ...config, encodeUrl: !(config.encodeUrl ?? true) })}
/>
</EuiFormRow>
</EuiPanel>
</EuiAccordion>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
export type UrlDrilldownConfig = {
url: { format?: 'handlebars_v1'; template: string };
openInNewTab: boolean;
encodeUrl?: boolean;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ test('should compile url without variables', () => {
expect(compile(url, {})).toBe(url);
});

test('by default, encodes URI', () => {
const url = 'https://elastic.co?foo=head%26shoulders';
expect(compile(url, {})).not.toBe(url);
expect(compile(url, {})).toBe('https://elastic.co?foo=head%2526shoulders');
});

test('when URI encoding is disabled, should not encode URI', () => {
const url =
'https://xxxxx.service-now.com/nav_to.do?uri=incident.do%3Fsys_id%3D-1%26sysparm_query%3Dshort_description%3DHello';
expect(compile(url, {}, false)).toBe(url);
});

test('should fail on unknown syntax', () => {
const url = 'https://elastic.co/{{}';
expect(() => compile(url, {})).toThrowError();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { encode, RisonValue } from 'rison-node';
import dateMath from '@elastic/datemath';
import moment, { Moment } from 'moment';
import numeral from '@elastic/numeral';
import { url } from '../../../../../../src/plugins/kibana_utils/public';

const handlebars = createHandlebars();

Expand Down Expand Up @@ -116,7 +117,22 @@ handlebars.registerHelper('replace', (...args) => {
return String(str).split(searchString).join(valueString);
});

export function compile(url: string, context: object): string {
const template = handlebars.compile(url, { strict: true, noEscape: true });
return encodeURI(template(context));
handlebars.registerHelper('encodeURIComponent', (component: unknown) => {
const str = String(component);
return encodeURIComponent(str);
});
handlebars.registerHelper('encodeURIQuery', (component: unknown) => {
const str = String(component);
return url.encodeUriQuery(str);
});

export function compile(urlTemplate: string, context: object, doEncode: boolean = true): string {
const handlebarsTemplate = handlebars.compile(urlTemplate, { strict: true, noEscape: true });
let processedUrl: string = handlebarsTemplate(context);

if (doEncode) {
processedUrl = encodeURI(processedUrl);
}

return processedUrl;
}