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 ``;
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) {