diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index df9fa2dca81fd..b292c1ae5e03f 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -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. + |=== diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts index e3730084d7020..1c6d7e4066187 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts @@ -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'); + }); +}); diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx index bfeab263d20e3..ffb0687305168 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx @@ -104,7 +104,8 @@ export class UrlDrilldown implements Drilldown ({ url: { template: '' }, - openInNewTab: false, + openInNewTab: true, + encodeUrl: true, }); public readonly isConfigValid = (config: Config): config is Config => { @@ -133,7 +134,12 @@ export class UrlDrilldown implements Drilldown = ({ inputRef={textAreaRef} /> - - onConfig({ ...config, openInNewTab: !config.openInNewTab })} - /> - + + + + + + onConfig({ ...config, openInNewTab: !config.openInNewTab })} + /> + + + + {txtUrlTemplateEncodeUrl} + + {txtUrlTemplateEncodeDescription} + + } + checked={config.encodeUrl ?? true} + onChange={() => onConfig({ ...config, encodeUrl: !(config.encodeUrl ?? true) })} + /> + + + ); }; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts index fb7d96aaf8325..59942ab6ff940 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts @@ -7,6 +7,7 @@ export type UrlDrilldownConfig = { url: { format?: 'handlebars_v1'; template: string }; openInNewTab: boolean; + encodeUrl?: boolean; }; /** diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts index 68a9654316d43..72c0a5ade7922 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts @@ -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(); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts index f4a1acff8762b..7533920d07d52 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts @@ -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(); @@ -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; }