diff --git a/src/core_plugins/kibana/common/field_formats/types/__tests__/url.js b/src/core_plugins/kibana/common/field_formats/types/__tests__/url.js index c7ac18064118e9..b1903c08ed3477 100644 --- a/src/core_plugins/kibana/common/field_formats/types/__tests__/url.js +++ b/src/core_plugins/kibana/common/field_formats/types/__tests__/url.js @@ -22,9 +22,9 @@ describe('UrlFormat', function () { describe('url template', function () { it('accepts a template', function () { - const url = new UrlFormat({ urlTemplate: 'url: {{ value }}' }); + const url = new UrlFormat({ urlTemplate: 'http://{{ value }}' }); expect(url.convert('url', 'html')) - .to.be('url: url'); + .to.be('http://url'); }); it('only outputs the url if the contentType === "text"', function () { @@ -35,9 +35,9 @@ describe('UrlFormat', function () { describe('label template', function () { it('accepts a template', function () { - const url = new UrlFormat({ labelTemplate: 'extension: {{ value }}' }); + const url = new UrlFormat({ labelTemplate: 'extension: {{ value }}', urlTemplate: 'http://www.{{value}}.com' }); expect(url.convert('php', 'html')) - .to.be('extension: php'); + .to.be('extension: php'); }); it('uses the label template for text formating', function () { @@ -79,4 +79,89 @@ describe('UrlFormat', function () { }); }); }); + + describe('whitelist', function () { + it('should assume a relative url if the value is not in the whitelist without a base path', function () { + const url = new UrlFormat(); + const parsedUrl = { + origin: 'http://kibana', + basePath: '', + }; + const converter = url.getConverterFor('html'); + + expect(converter('www.elastic.co', null, null, parsedUrl)) + .to.be('www.elastic.co'); + + expect(converter('elastic.co', null, null, parsedUrl)) + .to.be('elastic.co'); + + expect(converter('elastic', null, null, parsedUrl)) + .to.be('elastic'); + + expect(converter('ftp://elastic.co', null, null, parsedUrl)) + .to.be('ftp://elastic.co'); + }); + + it('should assume a relative url if the value is not in the whitelist with a basepath', function () { + const url = new UrlFormat(); + const parsedUrl = { + origin: 'http://kibana', + basePath: '/xyz', + }; + const converter = url.getConverterFor('html'); + + expect(converter('www.elastic.co', null, null, parsedUrl)) + .to.be('www.elastic.co'); + + expect(converter('elastic.co', null, null, parsedUrl)) + .to.be('elastic.co'); + + expect(converter('elastic', null, null, parsedUrl)) + .to.be('elastic'); + + expect(converter('ftp://elastic.co', null, null, parsedUrl)) + .to.be('ftp://elastic.co'); + }); + + it('should rely on parsedUrl', function () { + const url = new UrlFormat(); + const parsedUrl = { + origin: 'http://kibana.host.com', + basePath: '/abc', + }; + const converter = url.getConverterFor('html'); + + expect(converter('../app/kibana', null, null, parsedUrl)) + .to.be('../app/kibana'); + }); + + it('should fail gracefully if there are no parsedUrl provided', function () { + const url = new UrlFormat(); + + expect(url.convert('../app/kibana', 'html')) + .to.be('../app/kibana'); + + expect(url.convert('http://www.elastic.co', 'html')) + .to.be('http://www.elastic.co'); + }); + + it('should support multiple types of relative urls', function () { + const url = new UrlFormat(); + const parsedUrl = { + origin: 'http://kibana.host.com', + pathname: '/nbc/app/kibana#/discover', + basePath: '/nbc', + }; + const converter = url.getConverterFor('html'); + + expect(converter('#/foo', null, null, parsedUrl)) + .to.be('#/foo'); + + expect(converter('/nbc/app/kibana#/discover', null, null, parsedUrl)) + .to.be('/nbc/app/kibana#/discover'); + + expect(converter('../foo/bar', null, null, parsedUrl)) + .to.be('../foo/bar'); + }); + }); }); diff --git a/src/core_plugins/kibana/common/field_formats/types/url.js b/src/core_plugins/kibana/common/field_formats/types/url.js index 39ad248830c8a3..7a2d5dcd9b0ba8 100644 --- a/src/core_plugins/kibana/common/field_formats/types/url.js +++ b/src/core_plugins/kibana/common/field_formats/types/url.js @@ -2,6 +2,7 @@ import _ from 'lodash'; import { getHighlightHtml } from '../../highlight/highlight_html'; const templateMatchRE = /{{([\s\S]+?)}}/g; +const whitelistUrlSchemes = ['http://', 'https://']; export function createUrlFormat(FieldFormat) { class UrlFormat extends FieldFormat { @@ -83,7 +84,7 @@ export function createUrlFormat(FieldFormat) { return this._formatLabel(value); }, - html: function (rawValue, field, hit) { + html: function (rawValue, field, hit, parsedUrl) { const url = _.escape(this._formatUrl(rawValue)); const label = _.escape(this._formatLabel(rawValue, url)); @@ -98,6 +99,38 @@ export function createUrlFormat(FieldFormat) { return `${imageLabel}`; default: + const inWhitelist = whitelistUrlSchemes.some(scheme => url.indexOf(scheme) === 0); + if (!inWhitelist && !parsedUrl) { + return url; + } + + let prefix = ''; + /** + * This code attempts to convert a relative url into a kibana absolute url + * + * SUPPORTED: + * - /app/kibana/ + * - ../app/kibana + * - #/discover + * + * UNSUPPORTED + * - app/kibana + */ + if (!inWhitelist) { + // Handles urls like: `#/discover` + if (url[0] === '#') { + prefix = `${parsedUrl.origin}${parsedUrl.pathname}`; + } + // Handle urls like: `/app/kibana` or `/xyz/app/kibana` + else if (url.indexOf(parsedUrl.basePath || '/') === 0) { + prefix = `${parsedUrl.origin}`; + } + // Handle urls like: `../app/kibana` + else { + prefix = `${parsedUrl.origin}${parsedUrl.basePath}/app/`; + } + } + let linkLabel; if (hit && hit.highlight && hit.highlight[field.name]) { @@ -106,7 +139,7 @@ export function createUrlFormat(FieldFormat) { linkLabel = label; } - return `${linkLabel}`; + return `${linkLabel}`; } } }; diff --git a/src/core_plugins/kibana/public/field_formats/__tests__/_url.js b/src/core_plugins/kibana/public/field_formats/__tests__/_url.js index 2444766e594223..ce939569a8a47c 100644 --- a/src/core_plugins/kibana/public/field_formats/__tests__/_url.js +++ b/src/core_plugins/kibana/public/field_formats/__tests__/_url.js @@ -44,11 +44,11 @@ describe('Url Format', function () { describe('url template', function () { it('accepts a template', function () { - const url = new Url({ urlTemplate: 'url: {{ value }}' }); + const url = new Url({ urlTemplate: 'http://{{ value }}' }); const $a = unwrap($(url.convert('url', 'html'))); expect($a.is('a')).to.be(true); expect($a.size()).to.be(1); - expect($a.attr('href')).to.be('url: url'); + expect($a.attr('href')).to.be('http://url'); expect($a.attr('target')).to.be('_blank'); expect($a.children().size()).to.be(0); }); @@ -61,11 +61,11 @@ describe('Url Format', function () { describe('label template', function () { it('accepts a template', function () { - const url = new Url({ labelTemplate: 'extension: {{ value }}' }); + const url = new Url({ labelTemplate: 'extension: {{ value }}', urlTemplate: 'http://www.{{value}}.com' }); const $a = unwrap($(url.convert('php', 'html'))); expect($a.is('a')).to.be(true); expect($a.size()).to.be(1); - expect($a.attr('href')).to.be('php'); + expect($a.attr('href')).to.be('http://www.php.com'); expect($a.html()).to.be('extension: php'); }); @@ -109,5 +109,92 @@ describe('Url Format', function () { }); }); }); + + describe('whitelist', function () { + it('should assume a relative url if the value is not in the whitelist without a base path', function () { + const url = new Url(); + const parsedUrl = { + origin: 'http://kibana', + basePath: '', + }; + const converter = url.getConverterFor('html'); + + expect(converter('www.elastic.co', null, null, parsedUrl)) + .to.be('www.elastic.co'); + + expect(converter('elastic.co', null, null, parsedUrl)) + .to.be('elastic.co'); + + expect(converter('elastic', null, null, parsedUrl)) + .to.be('elastic'); + + expect(converter('ftp://elastic.co', null, null, parsedUrl)) + .to.be('ftp://elastic.co'); + }); + + it('should assume a relative url if the value is not in the whitelist with a basepath', function () { + const url = new Url(); + const parsedUrl = { + origin: 'http://kibana', + basePath: '/xyz', + }; + const converter = url.getConverterFor('html'); + + expect(converter('www.elastic.co', null, null, parsedUrl)) + .to.be('www.elastic.co'); + + expect(converter('elastic.co', null, null, parsedUrl)) + .to.be('elastic.co'); + + expect(converter('elastic', null, null, parsedUrl)) + .to.be('elastic'); + + expect(converter('ftp://elastic.co', null, null, parsedUrl)) + .to.be('ftp://elastic.co'); + }); + + it('should rely on parsedUrl', function () { + const url = new Url(); + const parsedUrl = { + origin: 'http://kibana.host.com', + basePath: '/abc', + }; + const converter = url.getConverterFor('html'); + + expect(converter('../app/kibana', null, null, parsedUrl)) + .to.be('../app/kibana'); + }); + + it('should fail gracefully if there are no parsedUrl provided', function () { + const url = new Url(); + const parsedUrl = null; + const converter = url.getConverterFor('html'); + + expect(converter('../app/kibana', null, null, parsedUrl)) + .to.be('../app/kibana'); + + expect(converter('http://www.elastic.co', null, null, parsedUrl)) + .to.be('http://www.elastic.co'); + }); + + it('should support multiple types of relative urls', function () { + const url = new Url(); + const parsedUrl = { + origin: 'http://kibana.host.com', + pathname: '/nbc/app/kibana#/discover', + basePath: '/nbc', + }; + const converter = url.getConverterFor('html'); + + expect(converter('#/foo', null, null, parsedUrl)) + .to.be('#/foo'); + + expect(converter('/nbc/app/kibana#/discover', null, null, parsedUrl)) + .to.be('/nbc/app/kibana#/discover'); + + expect(converter('../foo/bar', null, null, parsedUrl)) + .to.be('../foo/bar'); + }); + }); }); }); diff --git a/src/ui/field_formats/content_types.js b/src/ui/field_formats/content_types.js index 8371d7fa9dd3c0..7daa6fd9873038 100644 --- a/src/ui/field_formats/content_types.js +++ b/src/ui/field_formats/content_types.js @@ -4,17 +4,17 @@ import { getHighlightHtml } from '../../core_plugins/kibana/common/highlight/hig const types = { html: function (format, convert) { - function recurse(value, field, hit) { + function recurse(value, field, hit, meta) { if (value == null) { return asPrettyString(value); } if (!value || typeof value.map !== 'function') { - return convert.call(format, value, field, hit); + return convert.call(format, value, field, hit, meta); } const subVals = value.map(v => { - return recurse(v, field, hit); + return recurse(v, field, hit, meta); }); const useMultiLine = subVals.some(sub => { return sub.indexOf('\n') > -1; diff --git a/src/ui/public/index_patterns/_format_hit.js b/src/ui/public/index_patterns/_format_hit.js index c77c9803a6a3e2..b59b7d7f509015 100644 --- a/src/ui/public/index_patterns/_format_hit.js +++ b/src/ui/public/index_patterns/_format_hit.js @@ -1,4 +1,6 @@ import _ from 'lodash'; +import chrome from 'ui/chrome'; + // Takes a hit, merges it with any stored/scripted fields, and with the metaFields // returns a formatted version @@ -7,7 +9,12 @@ export function formatHit(indexPattern, defaultFormat) { function convert(hit, val, fieldName) { const field = indexPattern.fields.byName[fieldName]; if (!field) return defaultFormat.convert(val, 'html'); - return field.format.getConverterFor('html')(val, field, hit); + const parsedUrl = { + origin: window.location.origin, + pathname: window.location.pathname, + basePath: chrome.getBasePath(), + }; + return field.format.getConverterFor('html')(val, field, hit, parsedUrl); } function formatHit(hit) {