Skip to content

Commit

Permalink
[6.0] Prepend relative urls (#14994) (#15251)
Browse files Browse the repository at this point in the history
* Backport #14994

* Update tests
  • Loading branch information
chrisronline committed Nov 29, 2017
1 parent 3d9852e commit 4df8a0b
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 14 deletions.
Expand Up @@ -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('<span ng-non-bindable><a href="url: url" target="_blank">url: url</a></span>');
.to.be('<span ng-non-bindable><a href="http://url" target="_blank">http://url</a></span>');
});

it('only outputs the url if the contentType === "text"', function () {
Expand All @@ -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('<span ng-non-bindable><a href="php" target="_blank">extension: php</a></span>');
.to.be('<span ng-non-bindable><a href="http://www.php.com" target="_blank">extension: php</a></span>');
});

it('uses the label template for text formating', function () {
Expand Down Expand Up @@ -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('<span ng-non-bindable><a href="http://kibana/app/www.elastic.co" target="_blank">www.elastic.co</a></span>');

expect(converter('elastic.co', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana/app/elastic.co" target="_blank">elastic.co</a></span>');

expect(converter('elastic', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana/app/elastic" target="_blank">elastic</a></span>');

expect(converter('ftp://elastic.co', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana/app/ftp://elastic.co" target="_blank">ftp://elastic.co</a></span>');
});

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('<span ng-non-bindable><a href="http://kibana/xyz/app/www.elastic.co" target="_blank">www.elastic.co</a></span>');

expect(converter('elastic.co', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana/xyz/app/elastic.co" target="_blank">elastic.co</a></span>');

expect(converter('elastic', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana/xyz/app/elastic" target="_blank">elastic</a></span>');

expect(converter('ftp://elastic.co', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana/xyz/app/ftp://elastic.co" target="_blank">ftp://elastic.co</a></span>');
});

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('<span ng-non-bindable><a href="http://kibana.host.com/abc/app/../app/kibana" target="_blank">../app/kibana</a></span>');
});

it('should fail gracefully if there are no parsedUrl provided', function () {
const url = new UrlFormat();

expect(url.convert('../app/kibana', 'html'))
.to.be('<span ng-non-bindable>../app/kibana</span>');

expect(url.convert('http://www.elastic.co', 'html'))
.to.be('<span ng-non-bindable><a href="http://www.elastic.co" target="_blank">http://www.elastic.co</a></span>');
});

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('<span ng-non-bindable><a href="http://kibana.host.com/nbc/app/kibana#/discover#/foo" target="_blank">#/foo</a></span>');

expect(converter('/nbc/app/kibana#/discover', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana.host.com/nbc/app/kibana#/discover" target="_blank">/nbc/app/kibana#/discover</a></span>');

expect(converter('../foo/bar', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana.host.com/nbc/app/../foo/bar" target="_blank">../foo/bar</a></span>');
});
});
});
37 changes: 35 additions & 2 deletions src/core_plugins/kibana/common/field_formats/types/url.js
Expand Up @@ -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 {
Expand Down Expand Up @@ -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));

Expand All @@ -98,6 +99,38 @@ export function createUrlFormat(FieldFormat) {

return `<img src="${url}" alt="${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]) {
Expand All @@ -106,7 +139,7 @@ export function createUrlFormat(FieldFormat) {
linkLabel = label;
}

return `<a href="${url}" target="_blank">${linkLabel}</a>`;
return `<a href="${prefix}${url}" target="_blank">${linkLabel}</a>`;
}
}
};
Expand Down
95 changes: 91 additions & 4 deletions src/core_plugins/kibana/public/field_formats/__tests__/_url.js
Expand Up @@ -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);
});
Expand All @@ -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');
});

Expand Down Expand Up @@ -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('<span ng-non-bindable><a href="http://kibana/app/www.elastic.co" target="_blank">www.elastic.co</a></span>');

expect(converter('elastic.co', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana/app/elastic.co" target="_blank">elastic.co</a></span>');

expect(converter('elastic', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana/app/elastic" target="_blank">elastic</a></span>');

expect(converter('ftp://elastic.co', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana/app/ftp://elastic.co" target="_blank">ftp://elastic.co</a></span>');
});

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('<span ng-non-bindable><a href="http://kibana/xyz/app/www.elastic.co" target="_blank">www.elastic.co</a></span>');

expect(converter('elastic.co', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana/xyz/app/elastic.co" target="_blank">elastic.co</a></span>');

expect(converter('elastic', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana/xyz/app/elastic" target="_blank">elastic</a></span>');

expect(converter('ftp://elastic.co', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana/xyz/app/ftp://elastic.co" target="_blank">ftp://elastic.co</a></span>');
});

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('<span ng-non-bindable><a href="http://kibana.host.com/abc/app/../app/kibana" target="_blank">../app/kibana</a></span>');
});

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('<span ng-non-bindable>../app/kibana</span>');

expect(converter('http://www.elastic.co', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://www.elastic.co" target="_blank">http://www.elastic.co</a></span>');
});

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('<span ng-non-bindable><a href="http://kibana.host.com/nbc/app/kibana#/discover#/foo" target="_blank">#/foo</a></span>');

expect(converter('/nbc/app/kibana#/discover', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana.host.com/nbc/app/kibana#/discover" target="_blank">/nbc/app/kibana#/discover</a></span>');

expect(converter('../foo/bar', null, null, parsedUrl))
.to.be('<span ng-non-bindable><a href="http://kibana.host.com/nbc/app/../foo/bar" target="_blank">../foo/bar</a></span>');
});
});
});
});
6 changes: 3 additions & 3 deletions src/ui/field_formats/content_types.js
Expand Up @@ -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;
Expand Down
9 changes: 8 additions & 1 deletion 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

Expand All @@ -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) {
Expand Down

0 comments on commit 4df8a0b

Please sign in to comment.